copyable 0.2.0 → 0.3.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 6c394690b8922c1b1d88ff6665fb2b4e640d8459
4
- data.tar.gz: c7f7852188d8a62166b93063d85c77b8a1b1f31a
3
+ metadata.gz: 0ae89109e012130ce8cc01fa12b08ada0b3e1d09
4
+ data.tar.gz: a62a5cd9c1573d6e0da5f4a2d0d05b703f2af36b
5
5
  SHA512:
6
- metadata.gz: 5f5f9657c9506dd9583327da181245782944a57a54ced61627c87b3794aa851ba87e5a656aaeb391170c1b0cf31f5442a15398baa2931d2f7f2c918b88626f4d
7
- data.tar.gz: e7dc40c25c3b728d940c648b6a528cc9ba2b740d85b12307b30e40dc3135bed51387b6c0f005036ba8279886b8d1ff6b9efec652a58b7b9b105e0c97979d8dc8
6
+ metadata.gz: 25b7247a6471abb6114632dfa7818cb9bfd1ccc51d1ca89a9a6a60fce3057fbb55432135f67445a2ef5bc9590db975616235275e20c16ca6f339bf1d91383fc1
7
+ data.tar.gz: 1341e8a9c2101a8980af25a9b3eade8c2cc321448465cd7398a3e4df802a96cfd6bc27cf69d0e49beed8ba27e6d91ab825c9be46a34d90e659cb8bc5cdfc38eb
data/README.md CHANGED
@@ -121,16 +121,9 @@ The advice must be one of the following:
121
121
 
122
122
  ## Callbacks
123
123
 
124
- It depends on the situation as to whether you would want a particular callback to be fired when a model is copied. Since the logic of callbacks is situational, Copyable makes the decision to completely disable all callbacks and observers for the duration of the `create_copy!` method. The only exception are callbacks and observers related to validation.
125
-
126
- To make it easier to reason about the code, and for the sake of being obvious, every copyable declaration *must* include a declaration called `disable_all_callbacks_and_observers_except_validate`. This declaration itself does not do anything; it exists as documentation.
127
-
128
- copyable do
129
- disable_all_callbacks_and_observers_except_validate
130
- ...
131
- end
132
-
133
-
124
+ It depends on the situation as to whether you would want a particular callback to be fired when a model is copied. Since the logic of callbacks is situational, Copyable makes the decision to bypass callbacks
125
+ when saving the copied models by using raw SQL insert statements during the copy. It will check
126
+ validations unless `skip_validations` is passed to `create_copy`.
134
127
 
135
128
  ## The After Copy Declaration
136
129
 
@@ -267,4 +260,4 @@ So the recommended approach is to use `after_copy` to tweak the columns of the c
267
260
 
268
261
  ## About
269
262
 
270
- Copyable was developed at [District Management Group](https://dmgroupK12.com).
263
+ Copyable was developed at [District Management Group](https://dmgroupK12.com).
@@ -18,11 +18,11 @@ Gem::Specification.new do |spec|
18
18
  spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
19
  spec.require_paths = ["lib"]
20
20
 
21
- spec.add_dependency "activerecord", "~>4.1.0"
21
+ spec.add_dependency "activerecord", "~>4.2"
22
22
 
23
23
  spec.add_development_dependency "database_cleaner", "~> 1.4.0"
24
24
  spec.add_development_dependency "bundler", "~> 1.6"
25
25
  spec.add_development_dependency "rake"
26
26
  spec.add_development_dependency "rspec"
27
- spec.add_development_dependency "sqlite3"
27
+ spec.add_development_dependency "sqlite3", "~> 1.3.6"
28
28
  end
@@ -11,7 +11,7 @@ module Copyable
11
11
  @skip_associations = skip_associations
12
12
  @global_override = global_override
13
13
  association_list.each do |assoc_name, advice|
14
- association = original_model.class.reflections[assoc_name.to_sym]
14
+ association = original_model.class.reflections[assoc_name.to_s]
15
15
  check_advice(association, advice, original_model)
16
16
  unless advice == :do_not_copy || skip_associations.include?(assoc_name.to_sym)
17
17
  copy_association(association, original_model, new_model)
@@ -28,14 +28,14 @@ module Copyable
28
28
  message << "has unrecognized advice '#{advice}'."
29
29
  raise AssociationError.new(message)
30
30
  end
31
- if association.macro == :has_and_belongs_to_many && advice == :copy
31
+ if association.is_a?(ActiveRecord::Reflection::HasAndBelongsToManyReflection) && advice == :copy
32
32
  message = "Error in copyable:associations of "
33
33
  message << "#{original_model.class.name}: the association '#{association.name}' "
34
34
  message << "only supports the :copy_only_habtm_join_records advice, not the :copy advice, "
35
35
  message << "because it is a has_and_belongs_to_many association."
36
36
  raise AssociationError.new(message)
37
37
  end
38
- if association.macro != :has_and_belongs_to_many && advice == :copy_only_habtm_join_records
38
+ if !association.is_a?(ActiveRecord::Reflection::HasAndBelongsToManyReflection) && advice == :copy_only_habtm_join_records
39
39
  message = "Error in copyable:associations of "
40
40
  message << "#{original_model.class.name}: the association '#{association.name}' "
41
41
  message << "only supports the :copy advice, not the :copy_only_habtm_join_records advice, "
@@ -45,15 +45,14 @@ module Copyable
45
45
  end
46
46
 
47
47
  def copy_association(association, original_model, new_model)
48
- case association.macro
49
- when :has_many
48
+ if association.is_a?(ActiveRecord::Reflection::HasManyReflection)
50
49
  copy_has_many(association, original_model, new_model)
51
- when :has_one
50
+ elsif association.is_a?(ActiveRecord::Reflection::HasOneReflection)
52
51
  copy_has_one(association, original_model, new_model)
53
- when :has_and_belongs_to_many
52
+ elsif association.is_a?(ActiveRecord::Reflection::HasAndBelongsToManyReflection)
54
53
  copy_habtm(association, original_model, new_model)
55
54
  else
56
- raise "Unsupported association #{association.macro}" # should never happen, since we filter out belongs_to
55
+ raise "Unsupported association #{association.class}" # should never happen, since we filter out belongs_to
57
56
  end
58
57
  end
59
58
 
@@ -16,30 +16,33 @@ module Copyable
16
16
 
17
17
  def self.disable_all_callbacks(klass)
18
18
  klass.class_eval do
19
- # Don't do it if it was already done
20
- return if self.method_defined? :__disabled__run_callbacks
21
-
22
- alias_method :__disabled__run_callbacks, :run_callbacks
23
- # We are violently duck-punching ActiveRecord because ActiveRecord
24
- # gives us no way to turn off callbacks. My apologies to the
25
- # squeamish.
26
- def run_callbacks(kind, *args, &block)
27
- if block_given?
28
- yield
29
- else
30
- true
31
- end
19
+ return if self.method_defined? :old_save! # Don't do this more than once
20
+
21
+ @all_callbacks_disabled = true
22
+ class << self
23
+ attr_reader :all_callbacks_disabled
24
+ end
25
+
26
+ alias_method :old_save!, :save!
27
+
28
+ # Hold my beer while we bypass all the Rails callbacks
29
+ # in favor of some raw SQL
30
+ def save!
31
+ Copyable::Saver.save!(self)
32
32
  end
33
33
  end
34
34
  end
35
35
 
36
36
  def self.reenable_all_callbacks(klass)
37
37
  klass.class_eval do
38
- # Only do it if the disabled callbacks are defined
39
- return unless self.method_defined? :__disabled__run_callbacks
38
+ return unless self.method_defined? :old_save! # Don't do this more than once
39
+ @all_callbacks_disabled = false
40
+ class << self
41
+ attr_reader :all_callbacks_disabled
42
+ end
40
43
 
41
- alias_method :run_callbacks, :__disabled__run_callbacks
42
- remove_method :__disabled__run_callbacks
44
+ alias_method :save!, :old_save!
45
+ remove_method :old_save!
43
46
  end
44
47
  end
45
48
 
@@ -1,19 +1,27 @@
1
1
  module Copyable
2
2
  class Saver
3
3
 
4
+ def self.direct_sql_insert!(new_model)
5
+ new_model.send(:_create_record) # bypass all callbacks and validations
6
+ end
7
+
8
+ def self.direct_sql_update!(new_model)
9
+ new_model.send(:_update_record) # bypass all callbacks and validations
10
+ end
11
+
4
12
  # this is the algorithm for saving the new record
5
- def self.save!(new_model, skip_validations)
6
- unless skip_validations
7
- ModelHooks.reenable!(new_model.class) # we must re-enable or validation does not work
8
- if !new_model.valid?(:create)
9
- ModelHooks.disable!(new_model.class)
10
- raise(ActiveRecord::RecordInvalid.new(new_model))
11
- else
12
- ModelHooks.disable!(new_model.class)
13
- end
13
+ def self.save!(new_model, skip_validations=false)
14
+ unless skip_validations || new_model.valid?(:create)
15
+ raise(ActiveRecord::RecordInvalid.new(new_model))
14
16
  end
15
- new_model.save!
16
- end
17
17
 
18
+ if new_model.class.all_callbacks_disabled && new_model.id.nil?
19
+ self.direct_sql_insert!(new_model)
20
+ elsif new_model.class.all_callbacks_disabled
21
+ self.direct_sql_update!(new_model)
22
+ else
23
+ new_model.save!
24
+ end
25
+ end
18
26
  end
19
27
  end
@@ -10,7 +10,8 @@ module Copyable
10
10
  def expected_entries
11
11
  all_associations = model_class.reflect_on_all_associations
12
12
  required_associations = all_associations.select do |ass|
13
- (ass.macro != :belongs_to) && ass.options[:through].blank?
13
+ !ass.is_a?(ActiveRecord::Reflection::BelongsToReflection) &&
14
+ !ass.is_a?(ActiveRecord::Reflection::ThroughReflection)
14
15
  end
15
16
  required_associations.map(&:name).map(&:to_s)
16
17
  end
@@ -1,3 +1,3 @@
1
1
  module Copyable
2
- VERSION = "0.2.0"
2
+ VERSION = "0.3.0"
3
3
  end
@@ -39,7 +39,8 @@ task :copyable => :environment do
39
39
 
40
40
  all_associations = model_class.reflect_on_all_associations
41
41
  required_associations = all_associations.select do |ass|
42
- (ass.macro != :belongs_to) && ass.options[:through].blank?
42
+ !ass.is_a?(ActiveRecord::Reflection::BelongsToReflection) &&
43
+ !ass.is_a?(ActiveRecord::Reflection::ThroughReflection)
43
44
  end
44
45
  associations = required_associations.map(&:name).map(&:to_s)
45
46
  max_length = associations.map(&:length).max
@@ -11,14 +11,14 @@ module Copyable
11
11
 
12
12
  create_table :copyable_albums, force: true do |t|
13
13
  t.string :name, null: false
14
- t.timestamps
14
+ t.timestamps null: false
15
15
  end
16
16
 
17
17
  create_table :copyable_amenities, force: true do |t|
18
18
  t.string :name
19
19
  t.integer :copyable_vehicle_id
20
20
  t.integer :copyable_owner_id
21
- t.timestamps
21
+ t.timestamps null: false
22
22
  end
23
23
 
24
24
  create_table :copyable_addresses, force: true do |t|
@@ -27,30 +27,30 @@ module Copyable
27
27
  t.string :city, null: false
28
28
  t.string :state, null: false
29
29
  t.integer :copyable_pet_profile_id, null: false
30
- t.timestamps
30
+ t.timestamps null: false
31
31
  end
32
32
 
33
33
  create_table :copyable_cars, force: true do |t|
34
34
  t.string :make, null: false
35
35
  t.string :model, null: false
36
36
  t.integer :year, null: false
37
- t.timestamps
37
+ t.timestamps null: false
38
38
  end
39
39
 
40
40
  create_table :copyable_coins, force: true do |t|
41
41
  t.string :kind
42
42
  t.integer :year
43
- t.timestamps
43
+ t.timestamps null: false
44
44
  end
45
45
 
46
46
  create_table :copyable_owners, force: true do |t|
47
47
  t.string :name, null: false
48
- t.timestamps
48
+ t.timestamps null: false
49
49
  end
50
50
 
51
51
  create_table :copyable_pet_foods, force: true do |t|
52
52
  t.string :name, null: false
53
- t.timestamps
53
+ t.timestamps null: false
54
54
  end
55
55
 
56
56
  create_table :copyable_pet_foods_pets, force: true, id: false do |t|
@@ -62,31 +62,31 @@ module Copyable
62
62
  t.string :description, null: false
63
63
  t.string :nickname
64
64
  t.integer :copyable_pet_id, null: false
65
- t.timestamps
65
+ t.timestamps null: false
66
66
  end
67
67
 
68
68
  create_table :copyable_pet_sitters, force: true do |t|
69
69
  t.string :name, null: false
70
- t.timestamps
70
+ t.timestamps null: false
71
71
  end
72
72
 
73
73
  create_table :copyable_pet_sitting_patronages, force: true do |t|
74
74
  t.integer :copyable_pet_id, null: false
75
75
  t.integer :copyable_pet_sitter_id, null: false
76
- t.timestamps
76
+ t.timestamps null: false
77
77
  end
78
78
 
79
79
  create_table :copyable_pet_tags, force: true do |t|
80
80
  t.string :registered_name, null: false
81
81
  t.integer :copyable_pet_id, null: false
82
- t.timestamps
82
+ t.timestamps null: false
83
83
  end
84
84
 
85
85
  create_table :copyable_pets, force: true do |t|
86
86
  t.string :name, null: false
87
87
  t.string :kind, null: false
88
88
  t.integer :birth_year
89
- t.timestamps
89
+ t.timestamps null: false
90
90
  end
91
91
 
92
92
  create_table :copyable_pictures, force: true do |t|
@@ -94,36 +94,36 @@ module Copyable
94
94
  t.integer :imageable_id
95
95
  t.string :imageable_type
96
96
  t.integer :picture_album_id
97
- t.timestamps
97
+ t.timestamps null: false
98
98
  end
99
99
 
100
100
  create_table :copyable_products, force: true do |t|
101
101
  t.string :name
102
- t.timestamps
102
+ t.timestamps null: false
103
103
  end
104
104
 
105
105
  create_table :copyable_toys, force: true do |t|
106
106
  t.string :name
107
107
  t.string :kind
108
108
  t.integer :copyable_pet_id
109
- t.timestamps
109
+ t.timestamps null: false
110
110
  end
111
111
 
112
112
  create_table :copyable_trees, force: true do |t|
113
113
  t.string :kind
114
- t.timestamps
114
+ t.timestamps null: false
115
115
  end
116
116
 
117
117
  create_table :copyable_vehicles, force: true do |t|
118
118
  t.string :name
119
119
  t.integer :copyable_owner_id
120
- t.timestamps
120
+ t.timestamps null: false
121
121
  end
122
122
 
123
123
  create_table :copyable_warranties, force: true do |t|
124
124
  t.string :name
125
125
  t.integer :copyable_amenity_id
126
- t.timestamps
126
+ t.timestamps null: false
127
127
  end
128
128
 
129
129
  end
@@ -9,85 +9,31 @@ describe Copyable::ModelHooks do
9
9
  before(:each) do
10
10
  Copyable::ModelHooks.disable!(CopyableTree)
11
11
  end
12
+
12
13
  after(:each) do
13
14
  Copyable::ModelHooks.reenable!(CopyableTree)
14
15
  end
15
- it 'should prevent callbacks from executing' do
16
- expect {
17
- CopyableTree.create!(kind: 'magnolia')
18
- }.to_not raise_error
16
+
17
+ it 'defines an instance variable on the class' do
18
+ expect(CopyableTree.all_callbacks_disabled).to eq(true)
19
19
  end
20
+
20
21
  it 'should not prevent model actions from executing' do
21
22
  expect(CopyableTree.count).to eq(0)
22
- CopyableTree.create!(kind: 'magnolia')
23
- expect(CopyableTree.count).to eq(1)
24
23
  end
25
24
  end
26
25
 
27
26
  describe '.reenable!' do
28
- it 'should allow callbacks to execute again' do
29
- Copyable::ModelHooks.disable!(CopyableTree)
27
+ it 'defines an instance variable on the class' do
30
28
  Copyable::ModelHooks.reenable!(CopyableTree)
31
- expect {
32
- CopyableTree.create!(kind: 'magnolia')
33
- }.to raise_error(RuntimeError, "callback2 called")
29
+ expect(CopyableTree.all_callbacks_disabled).to eq(false)
34
30
  end
35
- end
36
- end
37
-
38
- context 'validations' do
39
31
 
40
- # Note: the relevant model and observer class is defined in helper/test_models.rb
41
-
42
- describe '.disable!' do
43
- before(:each) do
44
- Copyable::ModelHooks.disable!(CopyableCoin)
45
- end
46
- after(:each) do
47
- Copyable::ModelHooks.reenable!(CopyableCoin)
48
- end
49
- it 'should prevent validations from executing' do
50
- expect {
51
- CopyableCoin.create!(year: -10)
52
- }.to_not raise_error
53
- end
54
- it 'should not prevent model actions from executing' do
55
- expect(CopyableCoin.count).to eq(0)
56
- CopyableCoin.create!(year: -10)
57
- expect(CopyableCoin.count).to eq(1)
58
- end
59
- end
60
-
61
- describe '.reenable!' do
62
- it 'should allow validations to execute again' do
63
- Copyable::ModelHooks.disable!(CopyableCoin)
64
- Copyable::ModelHooks.reenable!(CopyableCoin)
32
+ it 'should allow callbacks to execute again' do
65
33
  expect {
66
- CopyableCoin.create!(year: -10)
67
- }.to raise_error(ActiveRecord::RecordInvalid)
34
+ CopyableTree.create!(kind: 'magnolia')
35
+ }.to raise_error(RuntimeError, "callback2 called")
68
36
  end
69
37
  end
70
38
  end
71
-
72
- describe 'nested disables and enables' do
73
- it 'should allow callbacks to execute again' do
74
- Copyable::ModelHooks.disable!(CopyableTree)
75
- expect { CopyableTree.create!(kind: 'magnolia') }.to_not raise_error
76
-
77
- Copyable::ModelHooks.disable!(CopyableCoin)
78
- expect { CopyableCoin.create!(year: -10) }.to_not raise_error
79
-
80
- Copyable::ModelHooks.disable!(CopyableTree)
81
- expect { CopyableTree.create!(kind: 'magnolia') }.to_not raise_error
82
-
83
- Copyable::ModelHooks.reenable!(CopyableCoin)
84
- expect { CopyableCoin.create!(year: -10) }.to raise_error(ActiveRecord::RecordInvalid)
85
-
86
- Copyable::ModelHooks.reenable!(CopyableTree)
87
- expect { CopyableTree.create!(kind: 'magnolia') }.to raise_error(RuntimeError)
88
-
89
- Copyable::ModelHooks.reenable!(CopyableTree)
90
- expect { CopyableTree.create!(kind: 'magnolia') }.to raise_error(RuntimeError)
91
- end
92
- end
93
39
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: copyable
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Wyatt Greene
@@ -11,7 +11,7 @@ authors:
11
11
  autorequire:
12
12
  bindir: bin
13
13
  cert_chain: []
14
- date: 2018-12-05 00:00:00.000000000 Z
14
+ date: 2019-03-18 00:00:00.000000000 Z
15
15
  dependencies:
16
16
  - !ruby/object:Gem::Dependency
17
17
  name: activerecord
@@ -19,14 +19,14 @@ dependencies:
19
19
  requirements:
20
20
  - - "~>"
21
21
  - !ruby/object:Gem::Version
22
- version: 4.1.0
22
+ version: '4.2'
23
23
  type: :runtime
24
24
  prerelease: false
25
25
  version_requirements: !ruby/object:Gem::Requirement
26
26
  requirements:
27
27
  - - "~>"
28
28
  - !ruby/object:Gem::Version
29
- version: 4.1.0
29
+ version: '4.2'
30
30
  - !ruby/object:Gem::Dependency
31
31
  name: database_cleaner
32
32
  requirement: !ruby/object:Gem::Requirement
@@ -87,16 +87,16 @@ dependencies:
87
87
  name: sqlite3
88
88
  requirement: !ruby/object:Gem::Requirement
89
89
  requirements:
90
- - - ">="
90
+ - - "~>"
91
91
  - !ruby/object:Gem::Version
92
- version: '0'
92
+ version: 1.3.6
93
93
  type: :development
94
94
  prerelease: false
95
95
  version_requirements: !ruby/object:Gem::Requirement
96
96
  requirements:
97
- - - ">="
97
+ - - "~>"
98
98
  - !ruby/object:Gem::Version
99
- version: '0'
99
+ version: 1.3.6
100
100
  description: Copyable makes it easy to copy ActiveRecord models.
101
101
  email:
102
102
  - dchan@dmgroupK12.com
@@ -178,7 +178,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
178
178
  version: '0'
179
179
  requirements: []
180
180
  rubyforge_project:
181
- rubygems_version: 2.4.6
181
+ rubygems_version: 2.4.8
182
182
  signing_key:
183
183
  specification_version: 4
184
184
  summary: ActiveRecord copier