Validation on associated object behaving differently on create and update - ruby-on-rails

I have a issue with a custom validation that checks the number of associated objects. There are two classes, Event and Guest.
When the user creates a new event, he can select some guests to be invited. Upon save, I want to do some check on the number of invited guests.
The validation works fine when updating an existing Event. But upon creation of a new event, the validation fails because self.guests.count returns 0.
In the controller, I checked that i the selected guests are passed to the model in the same way: params[:event][:guest_ids].
In case of an update, I can see that an SQL INSERT INTO to update the association table is added to the transaction queue before my validation block is called. But in case of a new event, the SQL INSERT doesn’t show up before the validation fails.
What am I missing here? I was thinking about some workarounds, but I’m sure there is a proper way to implement this.
Rails version is 4.2.6
class Event < ActiveRecord::Base
has_and_belongs_to_many :guests
validate :check_guestlist
def check_guestlist
guest_count=self.guests.count
if guest_count < 3
errors.add(“Please invite more guests”)
end
end
class Guest < ActiveRecord::Base
has_and_belongs_to_many :events
end
class EventsController < ApplicationController
def create
#event = current_user.events.new(event_params)
respond_to do |format|
if #event.save
format.html { redirect_to #event, notice: 'Event was successfully created.' }
else
flash.now[:alert]='Event not saved.'
format.html { render action: "new" }
end
end
end
def update
#event = current_user.events.find(params[:id])
respond_to do |format|
if #event.update_attributes(event_params)
format.html { redirect_to #event, notice: 'Event was successfully updated.' }
else
flash.now[:alert]='Changes not saved.'
format.html { render action: "edit" }
end
end
end
end

Ok the reason why self.guests.count doesn't work on create is that this is actually doing a database query, with the scope of event.
SELECT COUNT(*) FROMguestsWHEREguests.event_id= 1
Now of course this fails because the validation occurs before the guests have been saved. Now on update it works because there are entries to be found in the database. However the validation is checking against the wrong information - the count of guests in the database doesn't necessarily match the number of self.guests - you could have added one more guest, which has yet to be saved.
Your solution does work but personally I think it would be better to write it as:
validates :guests, length: { minimum: 3, message: 'Please invite more guests' }
length will test the size of the guests array - e.g. self.guests.length would be effectively the same as self.guests.to_a.count

I managed to work around this problem by replacing a line of code in the custom validation.
If I replace
guest_count=self.guests.count
with
guest_count=self.guests.to_a.count
I get the expected results.
In case of an update, both expressions return the same result. But in the context of a new record, only the second one responds as expected.
Nevertheless, I still don't understand why the first version didn't work.

Related

Associated records get overwritten when changing one record

When i try to edit the value of a column of one record, which is associated to a parent record (e.g. Jobs has many Jobdetails) every record gets overwritten instead of just this one record.
For example: a job has many jobdetails. I want to be able to edit the title of one jobdetail. when i save the record it works but every other jobdetail belonging to the job has the same title now. The creation of new records works without problems, only the editing is making troubles.
Models
Jobs has_many :jobdetails
Jobdetail belongs_to :job
Routes
resources :jobs do
resources :jobdetails
end
Jobdetails_controller.rb
def edit
#job = Job.find(params[:job_id])
#jobdetail = Jobdetail.find(params[:id])
end
def update
#job = Job.find(params[:job_id])
#jobdetail.update(jobdetail_params)
respond_to do |format|
if #job.jobdetails.update(jobdetail_params)
format.html { redirect_to job_jobdetail_path(#job, #jobdetail), notice: 'Jobdetail was successfully updated.' }
else
format.html { render :edit }
format.json { render json: #jobdetail.errors, status: :unprocessable_entity }
end
end
end
def set_jobdetail
#job = Job.find(params[:job_id])
#jobdetail = Jobdetail.find(params[:id])
end
i think the duplicated syntax in set_jobdetail and in the edit action are not necessary.
I tried several different syntaxes but they all wont work. thanks in advance!
Update the line that's doing a "massive" update:
if #job.jobdetails.update(jobdetail_params)
To do a single one:
if #jobdetail.update(jobdetail_params)
With #job.jobdetails.update you're getting all the jobdetails associated to #job, and updating all of them with the values from jobdetail_params. As you've already initialized the specific jobdetail you want to update (#jobdetail = Jobdetail.find(params[:id])), you must to invoke update on that object.

Keep changes on reload if validation fails

I'm working with validations in rails, stuff like:
validates_presence_of :some_field
I've noticed that if the validation fails, all changes are overwritten with existing values from the database. This makes some sense, as the page is basically being reloaded (as I gather from my development log), however this increases the risk of user error/frustration, as a single error in one field will require the hapless fellow to re-enter the changes he made to all fields.
My question: How can I get rails to reload the data that was just submitted if validation fails? That way, the user can correct the mistake without needing to re-enter the rest of his revisions.
Thanks for any advice.
Edit:
My update method, as requested, is as follows:
def update
#incorporation = Incorporation.find(params[:id])
#company = #incorporation.company
begin
#company.name="#{params[:company][:names_attributes].values.first["name_string"]} #{params[:company][:names_attributes].values.first["suffix"]}"
rescue NoMethodError
#company.name="Company #{#company.id} (Untitled)"
end
if #company.update(company_params)
redirect_to incorporations_index_path
else
redirect_to edit_incorporation_path(#incorporation)
end
end
Full disclosure regarding my controller: the above update is from my incorporations_controller even though I'm updating my Company model. Company has_one :incorporation. I did this because, in the larger context of my app, it made my associations much cleaner.
Update your controller to this
def update
#incorporation = Incorporation.find(params[:id])
#company = #incorporation.company
begin
#company.name="#{params[:company][:names_attributes].values.first["name_string"]} #{params[:company][:names_attributes].values.first["suffix"]}"
rescue NoMethodError
#company.name="Company #{#company.id} (Untitled)"
end
respond_to do |format|
if #company.update(company_params)
format.html { redirect_to({:action => "index"})}
else
format.html{render :edit}
format.json { render json: #incorporation.errors, status: :unprocessable_entity }
end
end
end
To add to the correct answer, you can clean up your code quite a bit:
def update
#incorporation = Incorporation.find params[:id]
respond_to do |format|
if #incorporation.update company_params
format.html { redirect_to({:action => "index"})}
else
format.html { render :edit }
format.json { render json: #incorporation.errors, status: :unprocessable_entity }
end
end
end
If you're using accepts_nested_attributes_for, you definitely should not hack the associated objects on the front-end.
You should look up fat model, skinny controller (let the model do the work):
#app/models/company.rb
class Company < ActiveRecord::Base
before_update :set_name
attr_accessor :name_string, :name_suffix
private
def set_name
if name_string && name_suffix
self[:name] = "#{name_string} #{name_suffix}"
else
self[:name] = "Company #{id} (Untitled)"
end
end
end
This will allow you to populate the name of the `company. To edit your nested/associated objects directly is an antipattern; a hack which will later come back to haunt you.
The key from the answer is: render :edit
Rendering the edit view means that your current #company / #incorporation data is maintained.
Redirecting will invoke a new instance of the controller, overriding the #incorporation, hence what you see on your front-end.

nested attributes using reject_if not working after render

I have a form with nested attributes and my model code contains:
accepts_nested_attributes_for :current_skills, :allow_destroy => true, reject_if: lambda {|attributes| attributes['skill_id'].blank?}
In my controller:
def edit
#employee = Employee.find(params[:id])
...
#employee.current_skills.build
#employee.desired_skills.build
end
def update
...
#employee.current_skills.build
#employee.desired_skills.build
if #employee.update_attributes(employee_params)
redirect_to #employee, notice: 'Employee was successfully updated.'
else
render :edit
end
end
In the case of a validation failure, the update, as expected, is not being saved and the edit view is being rendered. My understanding is that I need the .build methods in the update action to resupply the empty fields to the form (this is the case in my testing); however, when including the .build methods to supply the fields, they are not being rejected by the model on the second submit and are being saved to the database (not as I would expect).
Is there a better way to supply the fields after a validation failure but also have them rejected if blank?
Thanks for the help!
If I were you I would do this:
def update
...
if #employee.update_attributes(employee_params)
redirect_to #employee, notice: 'Employee was successfully updated.'
else
#employee.current_skills.build if #employee.current_skills.empty?
#employee.desired_skills.build if #employee.desired_skills.empty?
render :edit
end
end
I hope it helps.
Edit: After read your comment, I must to say: your code is fine and it must to work. Is not standard build an association in update method, and I can't see why you need it (if is for the tests, remove from main code and work on the tests). At this point, you must check the params at the server log, or a callback in yours models that set the skill_id value. BTW if the record saved have an skill_id not nil, your accept_nested_attributes_for is working fine.

Call Rails Model function from Controller

I'm new to Rails, and am trying to make a pet app. It has 3 attributes: name, hungry, and mood. I generated a scaffold and wrote a feed method into the model:
def feed
self.hungry==false;
save!
end
I want feed to be something a user can do in the edit view, so I created a checkbox to indicate feeding vs. not feeding. My plan was to call the feed function from the controller in the update function. Right now, it looks like this:
def update
respond_to do |format|
if #pet.update(pet_params)
format.html { redirect_to #pet, notice: 'Pet was successfully updated. #{params[:feed]}' }
format.json { head :no_content }
else
format.html { render action: 'edit' }
format.json { render json: #pet.errors, status: :unprocessable_entity }
end
end
if #pet.update_attributes(params[:feed])
#pet.feed
end
end
I have an odd sense that I'm mixing metaphors here, but am not sure of the right course of action. I'm trying to call a function from my update function, and that doesn't seem to be working. It might have to do with the fact that "feed" isn't listed in my model's parameters, but I don't need it to be. I just need it to call a function. Help!
Your method definition is wrong. Instead of assigning a value, you are comparing equality.
def feed
self.hungry == false; # only one = should be used.
save!
end
There is a better way to do this, however:
class Pet
attr_accessor :feed_me
before_save :feed
def feed
hungry = false if feed_me
end
end
You should not need the controller check:
if #pet.update_attributes(params[:feed])
#pet.feed
end
Which is wrong, by the way. You need to check if the param[:feed] exists, not if the pet objet has updated correctly.
For this solution to work, you would need to add an attribute to your form:
= f.check_box :feed_me
Another way to do this would be to map the hungry attribute to the checkbox and just name the label feed:
= f.label :hungry, "Feed"
= f.checkbox :hungry
You could then go ahead and just remove the before_save, the attr_accessor, and the method self.feed.

Validation errors appearing before submitting information in Rails?

I have two models, Character and Initiative, and their relationship is Character has_one Initiative and Initiative belogns_to Character. I'm working on validation for Initiative, and I have it working, but the issue is all of my validation errors appear when creating a new Initiative record for a Character, before entering any information. Any ideas? Here's my code from Initiatives controller:
def new
#character = Character.find(params[:character_id])
#initiative = #character.create_initiative(params[:initiative])
end
def edit
#character = Character.find(params[:character_id])
#initiative = #character.initiative
end
def create
#character = Character.find(params[:character_id])
#initiative = #character.create_initiative(params[:initiative])
if #initiative.save
redirect_to character_path(#character), :notice => "initiative successfully created!"
else
render :action => "new"
end
end
def update
#character = Character.find(params[:character_id])
#initiative = #character.initiative
if #initiative.update_attributes(params[:initiative])
redirect_to character_path(#character), :notice => 'Initiative information was successfully updated.'
else
render :action => "edit"
end
end
And here's the validation itself from my model:
validates_presence_of :dex, :misc, :speed
validates_numericality_of :dex, :misc, :speed
I'm pretty sure the problem lies in the create or new methods, but I'm not sure why it's triggering the validation before a user enters any information. Any help? Maybe not a huge concern, since the code IS working, but I'd rather not display an error message before actually getting an error. Thanks!
shouldn't you be using build_initiative instead of create_initiative in your new action ? no need to save an object when sending to the user a form that intends to create it. Moreover, if your character has_one initiative, he can only have one so i doubt AR appreciates that you try to create another.
see http://guides.rubyonrails.org/association_basics.html#has_one-association-reference

Resources