Billy Blob Snortin Billy Blob Snortin - 7 months ago 8
Ruby Question

Model instances not flagged as changed after successful attribute update

I would like to do conditionally show/hide some elements in one of my views depending on if a

@project.update
changed attributes or not.

I am trying to understand WHY my model instances are not being marked as changed after a successful attribute update.

It appears that
ActiveModel::Dirty
is not properly indicating my model's attributes have changed or more likely I'm not using it properly.

Here is an annotated log of a
PATCH
request I make to my
ProjectController#update
action. In it you will see that the attributes change but the model instance does not reflect it. For what it's worth, the controller was generated by a Rails scaffold operation. There's nothing out of the ordinary.

#
# FORM SUBMITTED FROM BROWSER WITH A CHANGE TO THE ATTRIBUTE NAMED "title"
#
Started PATCH "/projects/2" for 127.0.0.1 at 2016-04-23 15:47:38 -0700
Processing by ProjectsController#update as HTML
Parameters: {"utf8"=>"✓", "authenticity_token"=>"0JH/fEKx+Qk6mOY+eVTteKQUKrZUVXroKzMxuztrTzE/voI+PtzmQnJLGVM5bgdmzJyHDpAon3dzcvvjJ3yEtQ==", "project"=>{"title"=>"changed"}, "commit"=>"Update Project", "id"=>"2"}
Project Load (0.1ms) SELECT "projects".* FROM "projects" WHERE "projects"."id" = ? LIMIT ? [["id", 2], ["LIMIT", 1]]

#
# DEBUGGER KICKS IN BEFORE THE UPDATE HAS HAPPENED
#
[40, 49] in app/controllers/projects_controller.rb
40:
41: # PATCH/PUT /projects/1
42: # PATCH/PUT /projects/1.json
43: def update
44: byebug
=> 45: respond_to do |format|
46: if @project.update(project_params)
47: format.html { redirect_to @project, notice: 'Project was successfully updated.' }
48: format.json { render :show, status: :ok, location: @project }
49: else

#
# PROJECT TITLE IS STILL UNMOLESTED
#
(byebug) @project
<Project id: 2, title: "ORIGINAL_TITLE", created_at: "2016-04-23 22:47:30", updated_at: "2016-04-23 22:47:30">
# PROVE PARAMS CONTAIN A CHANGED ATTRIBUTE
(byebug) project_params
<ActionController::Parameters {"title"=>"changed"} permitted: true>

#
# TRIGGER UPDATE AND PERSIST NEW TITLE
#
(byebug) @project.update(project_params)
(0.2ms) begin transaction
SQL (0.9ms) UPDATE "projects" SET "title" = ?, "updated_at" = ? WHERE "projects"."id" = ? [["title", "changed"], ["updated_at", 2016-04-23 22:48:13 UTC], ["id", 2]]
(3.5ms) commit transaction
true

#
# WAT?
#
(byebug) @project.changes
{}
(byebug) @project.changed?
false
(bye bug)


Here is my
ProjectsController#update
action (standard Rails scaffold):

# app/controllers/projects_controller.rb

# PATCH/PUT /projects/1
# PATCH/PUT /projects/1.json
def update
byebug
respond_to do |format|
if @project.update(project_params)
format.html { redirect_to @project, notice: 'Project was successfully updated.' }
format.json { render :show, status: :ok, location: @project }
else
format.html { render :edit }
format.json { render json: @project.errors, status: :unprocessable_entity }
end
end
end


The corresponding view form (also from generated scaffolding):

# app/views/projects/_form.html.erb

<%= form_for(project) do |f| %>
<% if project.errors.any? %>
<div id="error_explanation">
<h2><%= pluralize(project.errors.count, "error") %> prohibited this project from being saved:</h2>

<ul>
<% project.errors.full_messages.each do |message| %>
<li><%= message %></li>
<% end %>
</ul>
</div>
<% end %>

<div class="field">
<%= f.label :title %>
<%= f.text_field :title %>
</div>

<div class="actions">
<%= f.submit %>
</div>
<% end %>

Answer

It seems like you are trying to achieve something if there were any changes made during an update.

If that's the case, you could do something like this :

Use a after_save or after_update callback as necessary for the model and within your callback, if you check self.changes or self.changed? you'd get expected results.