207

I'm setting up an after_save callback in my model observer to send a notification only if the model's published attribute was changed from false to true. Since methods such as changed? are only useful before the model is saved, the way I'm currently (and unsuccessfully) trying to do so is as follows:

def before_save(blog)
  @og_published = blog.published?
end

def after_save(blog)
  if @og_published == false and blog.published? == true
    Notification.send(...)
  end
end

Does anyone have any suggestions as to the best way to handle this, preferably using model observer callbacks (so as not to pollute my controller code)?

Frederik Spang
  • 3,379
  • 1
  • 25
  • 43
modulaaron
  • 2,856
  • 3
  • 24
  • 28

7 Answers7

265

Rails 5.1+

Use saved_change_to_published?:

class SomeModel < ActiveRecord::Base
  after_update :send_notification_after_change

  def send_notification_after_change
    Notification.send(…) if (saved_change_to_published? && self.published == true)
  end

end

Or if you prefer, saved_change_to_attribute?(:published).

Rails 3–5.1

Warning

This approach works through Rails 5.1 (but is deprecated in 5.1 and has breaking changes in 5.2). You can read about the change in this pull request.

In your after_update filter on the model you can use _changed? accessor. So for example:

class SomeModel < ActiveRecord::Base
  after_update :send_notification_after_change

  def send_notification_after_change
    Notification.send(...) if (self.published_changed? && self.published == true)
  end

end

It just works.

Aaron Brager
  • 65,323
  • 19
  • 161
  • 287
Radek Paviensky
  • 8,316
  • 2
  • 30
  • 14
  • Forget what I said above - it DOESN'T work in Rails 2.0.5. So a useful addition to Rails 3. – stephenr Oct 05 '10 at 08:34
  • Do I need to use `self.published_changed?`, or can I use just `published_changed?`? – Tobias Cohen Oct 07 '10 at 02:46
  • should work without self but I'm using it to explicitly show that I'm working with attributes/methods of self – Radek Paviensky Oct 07 '10 at 14:41
  • I would like to just add that this is very nice, but not very scalable. In the event you need tens or even hundreds of these I would suggest using an [observer](http://rails-bestpractices.com/posts/19-use-observer) – yekta Feb 08 '13 at 14:59
  • 4
    I think after_update is deprecated now? Anyway, I tried this in an after_save hook and that seemed to work fine. (The changes() hash still hasn't been reset yet in an after_save, apparently.) – Tyler Rick Mar 14 '13 at 05:09
  • This didn't work for me in ActiveRecord 3.2.16. But [this answer](http://stackoverflow.com/a/9233960/480943) worked for me. – Ben Lee Feb 12 '14 at 02:48
  • Same experience as @TylerRick...changed status appears to be reset after the save, these days. – Alex Edelstein Apr 17 '15 at 18:37
  • @AlexEdelstein I think Tyler was saying that it still worked even though it shouldn't. I seem to be in the same boat as you where changed is reset after save. I'm not sure what version of rails this was implemented in. I think Jacek's answer below is useful as is this: http://api.rubyonrails.org/classes/ActiveModel/Dirty.html – Marklar Dec 20 '15 at 22:55
  • 3
    I had to include this line in model.rb file. _include ActiveModel::Dirty_ – coderVishal Jan 07 '16 at 05:50
  • 13
    In later versions of Rails you can add the condition to the `after_update` call: `after_update :send_notification_after_change, if: -> { published_changed? }` – Koen. Mar 30 '16 at 20:31
  • 12
    There will be some API changes in Rails 5.2. You'll have to do `saved_change_to_published?` or `saved_change_to_published` to fetch the change during the callback – alopez02 Apr 04 '18 at 10:04
224

For those who want to know the changes just made in an after_save callback:

Rails 5.1 and greater

model.saved_changes

Rails < 5.1

model.previous_changes

Also see: http://api.rubyonrails.org/classes/ActiveModel/Dirty.html#method-i-previous_changes

Kris
  • 19,188
  • 9
  • 91
  • 111
Jacek Głodek
  • 2,374
  • 1
  • 14
  • 8
  • 3
    This works perfectly when you don't want to use model callbacks and need a valid save to happen before carrying out further functionality. – Dave Robertson Jan 20 '15 at 03:39
  • 5
    Just to be clear: in my tests (Rails 4), if you are using an `after_save` callback, `self.changed?` is `true` and `self.attribute_name_changed?` is also `true`, but `self.previous_changes` returns an empty hash. – sandre89 Nov 24 '16 at 04:54
  • looks like _previous_changes_ is a Rails 5 function. You can implement this in Rails 4 by adding an _attr_accessor_ _:previous_change_, fill it in a before_save call and access it again in an after_save. – Gernot Ullrich Dec 08 '16 at 15:09
  • @GernotUllrich Also works in Rails 4.2: http://devdocs.io/rails~4.2/activemodel/dirty#method-i-previous_changes – Michael Koper Jan 05 '17 at 16:59
  • 11
    This is deprecated in Rails 5.1 +. Use `saved_changes` in `after_save` callbacks instead – rico_mac Apr 26 '17 at 19:03
  • also for a single change ```instance.saved_change_to_published?``` – 1dolinski Jul 24 '19 at 13:59
86

To anyone seeing this later on, as it currently (Aug. 2017) tops google: It is worth mentioning, that this behavior will be altered in Rails 5.2, and has deprecation warnings as of Rails 5.1, as ActiveModel::Dirty changed a bit.

What do I change?

If you're using attribute_changed? method in the after_*-callbacks, you'll see a warning like:

DEPRECATION WARNING: The behavior of attribute_changed? inside of after callbacks will be changing in the next version of Rails. The new return value will reflect the behavior of calling the method after save returned (e.g. the opposite of what it returns now). To maintain the current behavior, use saved_change_to_attribute? instead. (called from some_callback at /PATH_TO/app/models/user.rb:15)

As it mentions, you could fix this easily by replacing the function with saved_change_to_attribute?. So for example, name_changed? becomes saved_change_to_name?.

Likewise, if you're using the attribute_change to get the before-after values, this changes as well and throws the following:

DEPRECATION WARNING: The behavior of attribute_change inside of after callbacks will be changing in the next version of Rails. The new return value will reflect the behavior of calling the method after save returned (e.g. the opposite of what it returns now). To maintain the current behavior, use saved_change_to_attribute instead. (called from some_callback at /PATH_TO/app/models/user.rb:20)

Again, as it mentions, the method changes name to saved_change_to_attribute which returns ["old", "new"]. or use saved_changes, which returns all the changes, and these can be accessed as saved_changes['attribute'].

Frederik Spang
  • 3,379
  • 1
  • 25
  • 43
  • 2
    Note this helpful response also includes workarounds for the deprecation of `attribute_was` methods: use `saved_change_to_attribute` instead. – Joe Atzberger Jul 24 '18 at 21:04
51

In case you can do this on before_save instead of after_save, you'll be able to use this:

self.changed

it returns an array of all changed columns in this record.

you can also use:

self.changes

which returns a hash of columns that changed and before and after results as arrays

mahemoff
  • 44,526
  • 36
  • 160
  • 222
zeacuss
  • 2,563
  • 2
  • 28
  • 32
  • 9
    Except that these don't work in an `after_` callback, which is what the question was actually about. @jacek-głodek's answer below is the correct one. – Jazz Dec 22 '14 at 21:26
  • Updated the answer to make it clear this only applies to `before_save` – mahemoff Apr 04 '15 at 13:00
  • 1
    Which Rails version is this? In Rails 4, `self.changed` can be used in `after_save` callbacks. – sandre89 Nov 24 '16 at 04:55
  • Works great! Also to be noted, the result of `self.changed` is an array of strings! (Not symbols!) `["attr_name", "other_attr_name"]` – LukeS Mar 02 '17 at 23:18
8

The "selected" answer didn't work for me. I'm using rails 3.1 with CouchRest::Model (based on Active Model). The _changed? methods don't return true for changed attributes in the after_update hook, only in the before_update hook. I was able to get it to work using the (new?) around_update hook:

class SomeModel < ActiveRecord::Base
  around_update :send_notification_after_change

  def send_notification_after_change
    should_send_it = self.published_changed? && self.published == true

    yield

    Notification.send(...) if should_send_it
  end

end
Jeff Gran
  • 1,585
  • 15
  • 17
  • 1
    The selected answer also didn't work for me, but this did. Thanks! I'm using ActiveRecord 3.2.16. – Ben Lee Feb 12 '14 at 02:47
7

you can add a condition to the after_update like so:

class SomeModel < ActiveRecord::Base
  after_update :send_notification, if: :published_changed?

  ...
end

there's no need to add a condition within the send_notification method itself.

echo
  • 900
  • 1
  • 9
  • 17
-22

You just add an accessor who define what you change

class Post < AR::Base
  attr_reader :what_changed

  before_filter :what_changed?

  def what_changed?
    @what_changed = changes || []
  end

  after_filter :action_on_changes

  def action_on_changes
    @what_changed.each do |change|
      p change
    end
  end
end
shingara
  • 46,608
  • 11
  • 99
  • 105