Alessio Caiazza is sharing code with you

Bitbucket is a code hosting site. Unlimited public and private repositories. Free for small teams.

Don't show this again

nolith / habtm-with-deferred-save

Rails plugin. Defers saving the records you add to habtm association until you call model.save, allowing validation in the style of normal attributes.

Clone this repository (size: 195.2 KB): HTTPS / SSH
hg clone https://bitbucket.org/nolith/habtm-with-deferred-save
hg clone ssh://hg@bitbucket.org/nolith/habtm-with-deferred-save

habtm-with-deferred-save / has_and_belongs_to_many_with_deferred_save / lib / has_and_belongs_to_many_with_deferred_save.rb

commit
c7287bf557dd
parent
d8a7e572f5cc
branch
default

FIX: delete all the assocation into the join table after obj.destroy

1
2498251996b3
# To do: make it work to call this twice in a class. Currently that probably wouldn't work, because it would try to alias methods to existing names...
2
2498251996b3
# Note: before_save must be defined *before* including this module, not after.
3
2498251996b3
4
2498251996b3
module ActiveRecord
5
2498251996b3
  module Associations
6
2498251996b3
    module ClassMethods
7
2498251996b3
      # Instructions:
8
2498251996b3
      #
9
2498251996b3
      # Replace your existing call to has_and_belongs_to_many with has_and_belongs_to_many_with_deferred_save.
10
2498251996b3
      #
11
2498251996b3
      # Then add a validation method that adds an error if there is something wrong with the (unsaved) collection. This will prevent it from being saved if there are any errors.
12
2498251996b3
      #
13
2498251996b3
      # Example:
14
2498251996b3
      #
15
2498251996b3
      #  def validate
16
2498251996b3
      #    if people.size > maximum_occupancy
17
2498251996b3
      #      errors.add :people, "There are too many people in this room"
18
2498251996b3
      #    end
19
2498251996b3
      #  end
20
2498251996b3
      def has_and_belongs_to_many_with_deferred_save(*args)
21
2498251996b3
        has_and_belongs_to_many *args
22
2498251996b3
        collection_name = args[0].to_s
23
356177366517
        collection_singular_ids = collection_name.singularize + "_ids"
24
c7287bf557dd
        
25
c7287bf557dd
        # this will delete all the assocation into the join table after obj.destroy
26
c7287bf557dd
        after_destroy { |record| record.save }
27
2498251996b3
28
2498251996b3
        attr_accessor :"unsaved_#{collection_name}"
29
3ed2e6884cb2
        attr_accessor :"use_original_collection_reader_behavior_for_#{collection_name}"
30
2498251996b3
31
2498251996b3
        define_method "#{collection_name}_with_deferred_save=" do |collection|
32
2498251996b3
          #puts "has_and_belongs_to_many_with_deferred_save: #{collection_name} = #{collection.collect(&:id).join(',')}"
33
2498251996b3
          self.send "unsaved_#{collection_name}=", collection
34
2498251996b3
        end
35
2498251996b3
36
2498251996b3
        define_method "#{collection_name}_with_deferred_save" do |*args|
37
3ed2e6884cb2
          if self.send("use_original_collection_reader_behavior_for_#{collection_name}")
38
2498251996b3
            self.send("#{collection_name}_without_deferred_save")
39
2498251996b3
          else
40
2498251996b3
            if self.send("unsaved_#{collection_name}").nil?
41
2498251996b3
              send("initialize_unsaved_#{collection_name}", *args)
42
2498251996b3
            end
43
2498251996b3
            self.send("unsaved_#{collection_name}")
44
2498251996b3
          end
45
2498251996b3
        end
46
2498251996b3
47
2498251996b3
        alias_method_chain :"#{collection_name}=", 'deferred_save'
48
2498251996b3
        alias_method_chain :"#{collection_name}", 'deferred_save'
49
2498251996b3
50
356177366517
        define_method "#{collection_singular_ids}_with_deferred_save" do |*args|
51
356177366517
          if self.send("use_original_collection_reader_behavior_for_#{collection_name}")
52
356177366517
            self.send("#{collection_singular_ids}_without_deferred_save")
53
356177366517
          else
54
356177366517
            if self.send("unsaved_#{collection_name}").nil?
55
356177366517
              send("initialize_unsaved_#{collection_name}", *args)
56
356177366517
            end
57
356177366517
            self.send("unsaved_#{collection_name}").map { |e| e[:id] }
58
356177366517
          end
59
356177366517
        end
60
356177366517
61
356177366517
        alias_method_chain :"#{collection_singular_ids}", 'deferred_save'
62
356177366517
63
2498251996b3
64
3ed2e6884cb2
        define_method "before_save_with_deferred_save_for_#{collection_name}" do
65
2498251996b3
          # Question: Why do we need this @use_original_collection_reader_behavior stuff?
66
2498251996b3
          # Answer: Because AssociationCollection#replace(other_array) performs a diff between current_array and other_array and deletes/adds only 
67
2498251996b3
          # records that have changed.
68
2498251996b3
          # In order to perform that diff, it needs to figure out what "current_array" is, so it calls our collection_with_deferred_save, not 
69
2498251996b3
          # knowing that we've changed its behavior. It expects that method to return the elements of that collection that are in the *database*
70
2498251996b3
          # (the original behavior), so we have to provide that behavior...  If we didn't provide it, it would end up trying to take the diff of
71
2498251996b3
          # two identical collections so nothing would ever get saved.
72
2498251996b3
          # But we only want the old behavior in this case -- most of the time we want the *new* behavior -- so we use 
73
2498251996b3
          # @use_original_collection_reader_behavior as a switch.
74
3ed2e6884cb2
          
75
3ed2e6884cb2
          if self.respond_to? :"before_save_without_deferred_save_for_#{collection_name}"
76
3ed2e6884cb2
            self.send("before_save_without_deferred_save_for_#{collection_name}") 
77
3ed2e6884cb2
          end  
78
2498251996b3
79
3ed2e6884cb2
          self.send "use_original_collection_reader_behavior_for_#{collection_name}=", true
80
d8a7e572f5cc
          if self.send("unsaved_#{collection_name}").nil?
81
d8a7e572f5cc
            send("initialize_unsaved_#{collection_name}", *args)
82
d8a7e572f5cc
          end
83
2498251996b3
          self.send "#{collection_name}_without_deferred_save=", self.send("unsaved_#{collection_name}")
84
2498251996b3
            # /\ This is where the actual save occurs.
85
3ed2e6884cb2
          self.send "use_original_collection_reader_behavior_for_#{collection_name}=", false
86
2498251996b3
87
2498251996b3
          true
88
2498251996b3
        end
89
3ed2e6884cb2
        alias_method_chain :"before_save", "deferred_save_for_#{collection_name}"
90
2498251996b3
91
2498251996b3
92
3ed2e6884cb2
        define_method "reload_with_deferred_save_for_#{collection_name}" do
93
2498251996b3
          # Reload from the *database*, discarding any unsaved changes.
94
3ed2e6884cb2
          returning self.send("reload_without_deferred_save_for_#{collection_name}") do
95
2498251996b3
            self.send "unsaved_#{collection_name}=", nil
96
2498251996b3
              # /\ If we didn't do this, then when we called reload, it would still have the same (possibly invalid) value of
97
2498251996b3
              # unsaved_collection that it had before the reload.
98
2498251996b3
          end
99
2498251996b3
        end
100
3ed2e6884cb2
        alias_method_chain :"reload", "deferred_save_for_#{collection_name}"
101
2498251996b3
102
2498251996b3
103
2498251996b3
        define_method "initialize_unsaved_#{collection_name}" do |*args|
104
2498251996b3
          #puts "Initialized to #{self.send("#{collection_name}_without_deferred_save").clone.inspect}"
105
2498251996b3
          self.send "unsaved_#{collection_name}=", self.send("#{collection_name}_without_deferred_save", *args).clone
106
2498251996b3
            # /\ We initialize it to collection_without_deferred_save in case they just loaded the object from the 
107
2498251996b3
            # database, in which case we want unsaved_collection to start out with the "saved collection".
108
2498251996b3
            # If they just constructed a *new* object, this will still work, because self.collection_without_deferred_save.clone 
109
2498251996b3
            # will return a new HasAndBelongsToManyAssociation (which acts like an empty array, []).
110
2498251996b3
            # Important: If we don't use clone, then it does an assignment by reference and any changes to unsaved_collection
111
2498251996b3
            # will also change *collection_without_deferred_save*! (Not what we want! Would result in us saving things
112
2498251996b3
            # immediately, which is exactly what we're trying to avoid.)
113
356177366517
          
114
356177366517
          # trick collection_name.include?(obj)
115
356177366517
          # If you use a collection of SignelTableInheritance and didn't :select 'type' the
116
356177366517
          # include? method will not find any subclassed object.
117
356177366517
          class << eval("@unsaved_#{collection_name}")
118
356177366517
            def include_with_deferred_save?(obj)
119
356177366517
              if self.find { |itm| itm == obj || (itm[:id] == obj[:id] && obj.is_a?(itm.class) ) }
120
356177366517
                return true
121
356177366517
              else
122
356177366517
                return false
123
356177366517
              end
124
356177366517
            end
125
356177366517
            alias_method_chain :include?, 'deferred_save'
126
356177366517
          end
127
2498251996b3
        end
128
356177366517
        private :"initialize_unsaved_#{collection_name}"
129
356177366517
        
130
2498251996b3
      end
131
2498251996b3
    end
132
2498251996b3
  end
133
2498251996b3
end