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.
Related
I'm new to wicked form and I was following the railcast episode on wicked forms but I keep receiving this error "Couldn't find Company with 'id'=info". So I know that the problem is clearly in my controllers somewhere. I know it's something super simple that I'm just racking my brain on so I know you guys will be a giant help. Here is the code, any and all help appreciated!
Code for companies Controller:
def create
#company = Company.new(company_params)
respond_to do |format|
if #company.save
#object = #company.id
format.html { redirect_to(company_steps_path(#company)) }
format.json { render :show, status: :created, location: #company }
else
format.html { render :new }
format.json { render json: #company.errors, status: :unprocessable_entity }
end
end
end
Code for company_steps Controller:
class CompanyStepsController < ApplicationController
include Wicked::Wizard
steps :info, :address, :quote
def show
#company = Company.find(params[:id])
render_wizard
end
def update
#company = Company.where(id: params[:id])
#company.attributes = params[:company]
render_wizard #company
end
end
When you use #find and the record is not found ActiveRecord raise a ActiveRecord::RecordNotFound with a message like "Couldn't find Company with id='somevalue'".
I assume your id column is of type integer and you pass a string.
In your #show method params[:id] == 'info'.
Check your link_to, redirect_to and routes.
At some point you generate this url http://localhost:3000/company_steps/info (probably in a view).
You do a GET request on it, which match GET "/company_steps/:id" company_steps#show.
The method #show is call in the controller CompanyStepsController with params[:id] == 'info'.
As we see previously you get a ActiveRecord::RecordNotFound exception because ActiveRecord can't find the record with a id 'info'.
The error is raise in your controller, but the problem is probably in your views or in a redirect. You need a id and you pass a string.
EDIT: as discussed in comments
Ok params[:id] == 'info' is generated by wicked.
They use id to control the flow of steps.
You need to use nested routes to have rails generate something like params[:company_id].
resources :companies do
resources :steps, controller: 'companies/steps'
end
So rake routes should give you:
/companies/:company_id/steps/:id
in the controller
params[:company_id] == 42
params[:id] == 'info'
https://github.com/schneems/wicked/wiki/Building-Partial-Objects-Step-by-Step
I have a few hours with something that is probably very easy.
I have a nested model
resources :grades do
resources :students
end
So I defined
before_action :set_grade, except: [:mass_input]
to my students_controller
def set_grade
#grade = Grade.find(params[:grade_id])
end
I'm very good with this, the problem is that now I'm using another action that takes :grade_id from another source, so I cant use set_grade, instead I'm passing the id with javascript. Works.
My problem appears here, when I try to call to create method, I'm probably doing it wrong ..
def mass_input
#grade = Grade.find(#data['grade'])
#data = JSON.parse(params[:form_data])
#is this create way ok or I'm overriding???
Student.create(:rut => #data['mass_students'][1][0], :nombre => #data['mass_students'][1][1], :apellido => #data['mass_students'][1][2])
end
This is my create action
def create
#student = Student.new(student_params)
#grade.students << #student
respond_to do |format|
if #student.save
format.html { redirect_to school_grade_path(#grade.school,#grade), notice: 'Alumno creado con éxito.' }
format.json { render :show, status: :created, location: #student }
else
format.html { render :new }
format.json { render json: #student.errors, status: :unprocessable_entity }
end
end
end
By this way code works but this line is not working
#grade.students << #student
#grade is not passing from mass_input to create. I think I'm not calling create properly but I cant find how to do it , because is not redirecting neither
My mass_input action is working by this way
def mass_input
#grade = Grade.find(#data['grade'])
#data = JSON.parse(params[:form_data])
Student.create(:rut => #data['mass_students'][1][0], :nombre => #data['mass_students'][1][1], :apellido => #data['mass_students'][1][2])
grade.students << student
respond_to do |format|
if student.save
format.html { redirect_to school_grade_path(grade.school,grade), notice: 'Alumno creado con éxito.' }
format.json { render :show, status: :created, location: student }
else
format.html { render :new }
format.json { render json: student.errors, status: :unprocessable_entity }
end
end
end
but I think is AWFUL, I must use my own create action
Thanks!!
Oh... From my point of view you are doing smth strange... The fast solution for your issue would be smth like this:
1) Rewrite before action in a new way:
before_action :set_grade
And method set_grade:
def set_grade
#grade = Grade.find(params[:grade_id].presence || #data['grade'])
end
2) Set method for student params
def student_params
data = JSON.parse(params[:form_data])['mass_students']
#Transform data to be student params. For ex:
data.map{|_key, info| {:rut => info[0], :nombre => info[1], :apellido => info[2]}}
end
3) Rewrite mass_input method
def mass_input
respond_to do |format|
if (#students = #grade.students.create(student_params).all?(&:persisted?)
#some actions when everything is great.
else
#some actions if not of them valid (maybe redirect & show info about not created students)
end
end
end
But you should definetly read more rails guides... http://guides.rubyonrails.org/
Sorry, I couldn't comment it. So I can just post a reply, it is not an complete answer though. In the student controller
Try to use
#student = #grade.students.new
or
#student = Student.new
#student.grade = #grade or #student.grade_id = params[:grade_id]
So when you do #student.save, you won't need to do the line below, and it will still work
#grade.students << #student
Ruby on rails has conventions you should follow to simplify lots of things. The first thing I see here is that in your def mass_input, you are using
Student.create(...)
The method create, as it says, creates an object but also saves it into database. So you should have new instead of create because new does not save it to database, just instantiates it:
#student = Student.new
...inside def mass_input, and by default the submit action in your view will take your object to the create method (if the object is new it goes to create, other way it goes to update, thanks to Rails). For this you could take a look at http://guides.rubyonrails.org/action_controller_overview.html
About the line #grade.students << #student, I assume you are intending to add the newly created student to his grade. See this example of usage of nested resources when trying to create, edit or destroy http://railscasts.com/episodes/139-nested-resources. In any case, nested resources implies this:
class Grade < ActiveRecord::Base
has_many :student
end
class Student < ActiveRecord::Base
belongs_to :grade
end
So, in your model Student you should have a column to store the Grade of that student. And then in your params you should receive the actual grade and store it in the grade_id inside your #student.
If something is not clear, I suggest you to take a look at the nested resources guide http://guides.rubyonrails.org/routing.html#nested-resources
As a commentary, << is used to add "things" to the end of an array, i.e. if you want to quickly store in an array some info you use:
array = []
Student.all.each do |s|
array << s.name
end
It will store in the array all the names of your students. Obviously there is a simpler way to do this by doing this:
Student.pluck(:name)
I want to save two models in one controller action, or save neither, and return with the validation errors.
Is there a better way than this?
def update
#job = Job.find(params[:id])
#location = #job.location
#job.assign_attributes(job_params)
#location.assign_attributes(location_params)
#job.save unless #job.valid? # gets validation errors
#location.save unless #location.valid? # gets validation errors
if #job.valid? && #location.valid?
#job.save
#location.save
flash[:success] = "Changes saved."
redirect_to edit_job_path(#job)
else
render 'edit'
end
end
New version:
def update
#job = Job.find(params[:id])
#location = #job.location
begin
Job.transaction do
#job.assign_attributes(job_params)
#job.save!(job_params)
#location.assign_attributes(location_params)
#location.save!(location_params)
end
flash[:success] = "Changes saved."
redirect_to edit_job_path(#job)
rescue ActiveRecord::RecordInvalid => invalid
render 'edit'
end
end
Have a look at Active Record Nested Attributes.
Using Nested attributes, you can save associated record attributes through parent.If parent record fails, associated records won't be saved.!
the first thing you'd want to do is delete these two lines
#job.save unless #job.valid? # gets validation errors
#location.save unless #location.valid? # gets validation errors
and only keep the #save in the if statement. because if one of them is valid, but the other isn't, you'll still save the valid one to the db.
To answer your second question, is there a better way to do this? At first blush, it looks like a job for #accepts_nested_attributes_for. However, accepts_nested_attributes_for is somewhat notorious for being difficult to get working (really it just takes a fare amount of tinkering) and what you're currently doing should get you where you're trying to go, so it's up to you.
You can use validates_associated rails helper:
class Model < ActiveRecord::Base
has_one :location
validates_associated :location
end
Then:
if #job.save
#blah
else
#blah
end
Is enough without having to mess with ActiveRecord#Nested_attributes. It's fastest, but less cleaner. Your choice.
Reference:
http://apidock.com/rails/ActiveRecord/Validations/ClassMethods/validates_associated
I have something like issue tracking system where there are issues and they have some comments.
Now on one page I want to give user an option to edit some stuff of "issue" as well as add a comment. Editing of and issue is a standard stuff like in /edit but also I want to create a comment and validate if it's not blank.
I've figured out that I can build a comment and make a form for it, but how should I check simultaneously that both issue attributes and comment attributes are valid? Because each update should be followed by a new comment, but I don't want to create a new comment if the issue attributes are no valid.
I would approach this by first adding fails_validation? methods to both your Issues and Comments models to check for problems.
Second, you will have to manually load the #issue form data from params[] and validate it BEFORE you save it (can't use update_attributes(params[:issue]).) Create a new Comment and load it via params[]. Then you can test the validation on both models and go back to the edit action if either fails.
If both pass you can save #issue and then #comment as normal.
def update
#issue = Issue.find(params[:id])
# manually transfer form data to the issue model
#issue.title = params[:issue][:title]
#issue.body = params[:issue][:body]
#...
#comment = #issue.comments.new(params[:comment])
# validate both #issue and #comment
if #issue.fails_validation? || #comment.fails_validation?
flash[:error] = "Your edits or your comment did not pass validation."
render :action => "edit",
end
# validation passed, save #issue then #comment
respond_to do |format|
if #issue.save
#comment.save
format.html { redirect_to #issue, notice: 'Issue successfully updated. Comment created' }
format.json { head :ok }
else
format.html { render action: "edit" }
format.json { render json: #issue.errors, status: :unprocessable_entity }
end
end
end
Not the most elegant solution, but it should work.
You can validate the comment model and the issue model in their respective classes.
It is not clear to me whether you are using 'accepts_nested_attributes_for' in Issue for comments. If you are, then the standard IssueController#update will not save the record if issue is invalid and consequently, it will not create the comment records as well.
Here is the standard IssueController#update:
class IssueController < ApplicationController
def update
#issue = Issue.find(params[:id])
if #issue.update_attributes(params[:issue])
redirect_to issues_path, notice: 'issue updated'
else
render action: 'edit'
end
end
I have a Feature page that belongs to the Car page. That is working exactly how I want to, except for one thing.
After creating, updating or destroying, I want the page to be redirected to the admin_car_path(car) instead of the defaults admin_car_feature_path(car,feature) for create and update and admin_car_features_path(car).
I unsuccessfully searched for that.
ActiveAdmin.register Car do
end
ActiveAdmin.register Feature do
belongs_to :car
end
TIA
right code for updating without skipping validation
controller do
def update
super do |success,failure|
success.html { redirect_to collection_path }
end
end
end
Here is the code for update action for your case. This code goes to the features.rb - admin file:
controller do
def update
update! do |format|
format.html { redirect_to admin_cars_path }
end
end
end
This redirects to the cars index page. So you have the idea. Same for create and destroy actions.
At the current moment accepted answer leads to ignoring validation errors.
This works for me with the latest versions of ActiveAdmin and Rails:
controller do
def update
update! do |format|
format.html { redirect_to collection_path } if resource.valid?
end
end
def create
create! do |format|
format.html { redirect_to collection_path } if resource.valid?
end
end
end
Here is a solution that also works with create_another, using parent and child for model names.
This solution assumes that you show children as part of parent (e.g. via table_for) so you do not need child's index method.
In resource override controller's smart_resource_url and index methods:
controller do
def smart_resource_url
if create_another?
new_resource_url(create_another: params[:create_another])
else
parent_path(params[:parent_id])
end
end
def index
redirect_to parent_path(params[:parent_id])
end
end
Current answer is skipping validations. Some of the other answers are working but partially correct (incorrect use of super or manually validating resource).
Most updated "proper" way to redirect with AA after create and udpate:
controller do
def create
create! do |success,failure|
success.html { redirect_to collection_path, notice: "#{resource.model_name.human} was successfully created." }
end
end
def update
update! do |success,failure|
success.html { redirect_to collection_path, notice: "#{resource.model_name.human} was successfully updated." }
end
end
end
Marcelo, I'm not sure I understand your question, but wouldn't putting this into the update, create and destroy actions in your controller do the trick?
format.html { redirect_to redirect_address }
And make redirect_address whatever you need.