Rails validation over redirect - ruby-on-rails

I'm trying out the beast forum written in rails and will use this as an example of a problem I keep facing.
The forum has a topics/show action and view with a form at the bottom to create a new post within the topic.
Submitting the form goes to posts/create and if the validation passes redirects back to topics/show and works fine, however if the validation fails (leaving out the body field) you're redirected to the same topics/show and back to the form, with no validation errors... normally if validation fails you're left on whatever/create with render :action => new.
Are the validations being lost in the redirect, and what's the best method of getting it working?
See code below:
PostsController.rb
def create
#post = current_user.reply #topic, params[:post][:body]
respond_to do |format|
if #post.new_record?
format.html { redirect_to forum_topic_path(#forum, #topic) }
format.xml { render :xml => #post.errors, :status => :unprocessable_entity }
else
flash[:notice] = 'Post was successfully created.'
format.html { redirect_to(forum_topic_post_path(#forum, #topic, #post, :anchor => dom_id(#post))) }
format.xml { render :xml => #post, :status => :created, :location => forum_topic_post_url(#forum, #topic, #post) }
end
end
end
TopicsController.rb
def show
respond_to do |format|
format.html do
if logged_in?
current_user.seen!
(session[:topics] ||= {})[#topic.id] = Time.now.utc
end
#topic.hit! unless logged_in? && #topic.user_id == current_user.id
#posts = #topic.posts.paginate :page => current_page
#post = Post.new
end
format.xml { render :xml => #topic }
end
end
topics/show view
<% form_for :post, :url => forum_topic_posts_path(#forum, #topic, :page => #topic.last_page) do |f| %>
<%= f.error_messages %>
<table width="100%" border="0" cellpadding="0" cellspacing="0">
<tr>
<td rowspan="2" width="70%">
<%= f.text_area :body, :rows => 8 %>
</td>
<td valign="top">
<%= render :partial => "posts/formatting" %>
</td>
</tr>
<tr>
<td valign="bottom" style="padding-bottom:15px;">
<%= submit_tag I18n.t('txt.views_topics.save_reply', :default => 'Save reply') %>
</td>
</tr>
</table>
<% end %>
Many thanks.

I think you have two problems here.
Keeping validation errors through a redirect
Repopulating the form fields so the user doesn't have to enter all the information again.
Both of these things are connected.
Validation errors are usually displayed through the error_msg_for method which acts on an object. Usually provided by the controller as the an instance variable of object that could not be saved. That same instance variable is used to repopulate the form.
During a redirect the controller will usually instantiate an instance variable using the params hash. So any information used to determine why a save failed is lost. Normal resources will render on save failure and redirect on success, this causes two things happen.
The instance of the object is passed to error_msg_for creating that nice uniform error box.
The instance of the object is used to populate the fields of the form, allowing your user to edit only what is necessary.
I don't know Beast so well, so I'm not sure if the form to create threads is an active record model. But the following will give you an idea how to work around your problem. It would involve modifying your local copy of the Beast plugin, so if you're using a tool to keep it updated, your changes might get lost.
I've been using these following methods to get your validation problems. Assuming the form you're talking about is based on a nmodel they should provide you with everything you need to repopulate a form.
Essentially you store a shallow copy of the object with the errors in the flash hash, using clone_with_errors. You have to use a shallow copy or else you'll run into problems when displaying errors for records with multiple associations.
Then I use my_error_msg_for which takes the same options as the standard error_msg_for to build the error messages html. I only wrote it because for some reason the standard error_msg_for method didn't work with objects stored in the hash. It's almost identical to the official source version of error_msg_for which was troubling.
/app/controllers/examples_controller.rb
class ExamplesController < ApplicationController
def update
...
if #example.save
regular action
else
flash[:errors] = clone_with_errors(#example)
respond_to do |format|
format.html redirect_to(#example)
end
end
end
/app/views/examples/show.html.erb
<div id="error">
<% if flash[:errors] && !flash[:errors].empty? then -%>
<p ><%= my_error_msg_for flash[:errors] %></p>
<% end -%>
</div>
...
Here's the code you need to make it all work.
application_controller.rb
def clone_with_errors(object)
clone = object.clone
object.errors.each{|field,msg| clone.errors.add_to_base(msg)}
return clone
end
application_helper.rb
def _error_msg(*params)
options = params.extract_options!.symbolize_keys
if object = options.delete(:object)
objects = [object].flatten
else
objects = params.collect {|object_name| instance_variable_get("##{object_name}") }.compact
end
count = objects.inject(0) {|sum, this| sum + this.errors.count }
unless count.zero?
html = {}
[:id, :class].each do |key|
if options.include?(key)
value = options[key]
html[key] = value unless value.blank?
else
html[key] = 'errorExplanation'
end
end
options[:object_name] ||= params.first
options[:header_message] = "#{pluralize(count, 'error')} prohibited this #{options[:object_name].to_s.gsub('_', ' ')} from being saved" unless options.include?(:header_message) && !options[:header_messag].nil?
options[:message] ||= 'There were problems with the following fields:' unless options.include?(:message) && !options[:message].nil?
error_messages = objects.sum {|this| this.errors.full_messages.map {|msg| content_tag(:li, msg) } }.join
contents = ''
contents << content_tag(options[:header_tag] || :h2, options[:header_message]) unless options[:header_message].blank?
contents << content_tag(:p, options[:message]) unless options[:message].blank?
contents << content_tag(:ul, error_messages)
content_tag(:div, contents, html)
else
''
end
end
def my_error_msg_for(params)
_error_msg_test :object_name => params[:object].class.name.gsub(/([a-z])([A-Z])/,'\1 \2').gsub(/_/, " "),
:object => params[:object], :header_message => params[:header_message], :message => params[:message]
end

I'm afraid I don't know anything about Beast, but speaking generically, everything is lost when you redirect. It's a new page request, and everything is reset unless it's been stored somewhere (the database or the session, normally.)
The normal flow you tend to see with forms is to redirect if the object is saved, but to render if the save fails. The view file can then pick up whatever variables have been set in the controller - which would normally include the object that didn't save and its validation messages.
Sorry that doesn't solve your problem, but hopefully it may give you some clues.

My answer to a very similar question posted more recently here on StackOverflow covers a number of pros and cons to the redirect_to vs. render debate. I'd love to hear if anyone has any other pros/cons to add to the discussion.

Related

Rails: JSON round trip from database to TEXTAREA converts to string on second save

(Rails 5.2) My record with JSON is saving correctly to the (Postgres 9.4) database on CREATE but is getting reformatted after UPDATE, with carriage returns (\r) and newline characters (\n) showing up in the database after UPDATE.
The :budget_json column is a jsonb type.
Here is my (simplified) Budget model:
class Budget < ApplicationRecord
after_initialize :create_shell_json
def create_shell_json
self.budget_json = blank_budget if self.new_record?
end
def blank_budget
{
id: nil,
name: nil,
year: nil,
[etc...]
}
end
end
Here is my controller:
def new
#budget = Budget.new(year: Time.now.year)
end
def create
#budget = Budget.new(budget_params)
#budget.budget_json = JSON.parse(budget_params[:budget_json])
if #budget.save
redirect_to admin_budgets_path, notice: "Budget successfully created."
else
render :new
end
end
def edit
#budget = Budget.find(params[:id])
end
def update
#budget = Budget.find(params[:id])
#budget.budget_json = JSON.parse(budget_params[:budget_json])
if #budget.update(budget_params)
redirect_to admin_budgets_path, notice: "Budget successfully updated."
else
render :edit
end
end
And here are the relevant parts of the form. (The form is the same for CREATE and UPDATE.) The TEXTAREA contains the editable JSON should the user want to amend the default values:
<%= form_with model: [:admin, #budget], local: true, :html => {:class => "form-horizontal"} do |f| %>
...
<div class="form-group">
<div class="col-sm-2">
<%= f.label :budget_json %>
</div>
<div class="col-sm-2">
<%= text_area_tag "budget[budget_json]", JSON.pretty_generate(#budget.budget_json), id: "budget_budget_json" %>
</div>
</div>
...
<% end %>
FWIW, the form looks like this:
As you can see here (from pgAdmin), the first record (id: 166) is clean and usable. It has only just been created. The second record (id: 167) is unusable as has been stored as a string instead:
What am I missing?
Ran into something similar, but managed to get around by modifying the params before passing them to the update method. So in your case something like:
def update
params = budget_params
params[:budget_json] = JSON.parse(params[:budget_json])
if #budget.update(params)
redirect_to admin_budgets_path, notice: "Budget successfully updated."
else
render :edit
end
end
Jeez. How often does writing the whole thing out help you think more clearly! I have the answer: in the UPDATE action I wasn't actually using the JSON.parsed version of the parameters. By changing
if #budget.update(budget_params)
to
if #budget.save(budget_params)
everything works as it should.
Having said that, if anyone is able to suggest a more elegant way of coding these (admin interface) round trips for JSON data, I'll be happy to hear your suggestions.

Change local Ruby variable with Ajax in Ruby on Rails

I'm creating a scheduling page that shows a weekly schedule and want to make arrows at the bottom of the page that switch forward and backward a week. To do this I'm creating a variable in the controller, #wbeg, that defaults to the beginning of the week and two actions, week_after and week_before, with respective js.erb files. I can't figure out how to change the #wbeg variable using ajax and need to do so to keep everything on the same page.
Here is the controller:
class HomeController < ApplicationController
def home
end
def scheduling
#work_days = WorkDay.all
#jobs = Job.all
#wbeg = Date.today - Date.today.wday + 1 #beginning of this week
end
def week_before
respond_to do |format|
format.html { redirect_to scheduling_path notice: "You should not see this message, contact admin if you do."}
format.js
end
end
def week_after
respond_to do |format|
format.html { redirect_to scheduling_path notice: "You should not see this message, contact admin if you do."}
format.js
end
end
end
The scheduling page:
<p id="notice"><%= notice %></p>
<h1>Work Day Scheduling</h1>
<table>
<thead>
<tr>
<th>Job #</th>
<th>Job</th>
<th>Monday</th>
<th>Tuesday</th>
<th>Wednesday</th>
<th>Thursday</th>
<th>Friday</th>
<th>Saturday</th>
</tr>
</thead>
<tbody id = "main_table">
<%= render "home/main_table" %>
</tbody>
</table>
<%= link_to '<--', week_before_path, remote: true %>
<%= link_to '-->', week_after_path, remote: true %>
and the two js.erb pages:
$('#main_table').html("<%= j render 'main_table', locals: { wbeg: #wbeg - 7 } %>");
The other:
$('#main_table').html("<%= j render 'main_table', locals: { wbeg: #wbeg + 7 } %>");
I also already tried changing the variable in the week_before and after actions in the controller but it gives the same error 'nil cannot have operation "-"' or something, which just means it's not recognizing the variable.
The way you have it coded now, the value of the #wbeg variable at the time the javascript file is generated will be hard coded into the javascript file. That value will never change. Whatever it was when the javascript was generated is it.
What you need is a javascript variable that you can update in the javascript code which makes the AJAX call.

How to change value in params array in Ruby on Rails?

I have an instance where I am recording prices for water from vendors. My vendor model has :price. However, I want to give users the option to input a price for different volumes, and do the simple division for them rather than having them to do it. In other words, users should be able to input $1.99 per liter or $3.99 for a gallon and so on. To do this, I need a virtual attribute in my form for :unit, since I don't want to be storing units in the table. Everything works well, except that I cannot seem to update vendor_params[:price] before I update the record or create a new record. This seems like it should be a cake walk, but I Googled most of the day and can't figure out how to make it work.
Here is what I have:
Model:
class Vendor < ActiveRecord::Base
attr_accessor :unit
...
end
Form:
<%= form_for(#vendor) do |f| %>
...
<div class="field">
<%= f.label :price %><br>
<%= f.text_field :price %>
<%= select( "unit", "id", { "1 Liter" => "1", "Bottle (2 liters)" => "2", "Jerry Can (20 liters)" => "20"}) %>
</div>
...
<% end %>
Controller:
...
def update
vendor_params[:price] = vendor_params[:price].to_f/params[:unit][:id].to_f
respond_to do |format|
if #vendor.update(vendor_params)
format.html { redirect_to #vendor, notice: 'Vendor was successfully updated.' }
format.json { head :no_content }
else
format.html { render action: 'edit' }
format.json { render json: #vendor.errors, status: :unprocessable_entity }
end
end
...
end
I know that vendor_params[:price].to_f/params[:unit][:id].to_f returns the correct value. I just can't seem to assign that value to vendor_params[:price] before I update the record. I also tried the following which throws an error:
#vendor_params[:price] = vendor_params[:price].to_f/params[:unit][:id].to_f
It seems like this should be trivial! I guess I could use form_tag instead of form_for, but that seems odd when updating the full record. (The edit form has all fields for all the object attributes.) Anywho, I'm open to ideas and suggestions.
Thanks!!
If vendor_params is a strong_params method (which I'm assuming it is), it actually creates a new hash. So when you alter vendor_params... you're not actually changing your original params hash.
OK, why isn't vendor_params changing though... I dont care about params? WELL, vendor_params still points the original params hash assuming it looks something like:
def vendor_params
params.require(:vendor).permit(:price)
end
I think the link below is a similar issue and may present a useful solution. Hope I understood your problem correctly!
Modify ruby hash in place( rails strong params)

Routing a post says missing template

I'm rather confused about this; I have a custom route here.
I have a groups/:id/new_caretaker. This has a form on it. Whenever that form is POSTed it should go to the same page; But to a different method.
However, if I post the form it says Missing template groups/create_caretaker, application/create_caretaker
How can I fix this?
Here's my form:
<%= form_tag(controller: "groups", action: "create_caretaker", method: "post") do %>
<div class="field">
<%= text_field_tag('email') %>
</div>
<%= submit_tag "Opslaan"%>
<% end %>
And my routes:
get "/groups/:id/new_caretaker" => "groups#new_caretaker", :as => :new_caretaker
post "/groups/:id/new_caretaker" => "groups#create_caretaker"
Added groups.controller methods:
Note: new_caretaker gets #group from a :before_action
def new_caretaker
end
def create_caretaker
email = params[:email]
if !email.blank?
userToAdd = User.find_by_email(email)
if userToAdd.blank?
#User doesn't exist
else
#User does exist
respond_to do |format|
if #group.users.find_by_id(userToAdd)
#theAlert = 'Deze gebruiker zit al in de groep en is niet toegevoegd'
format.html { render action: 'new_caretaker' }
format.json { render json: #theAlert, status: :unprocessable_entity }
else
#group.users << userToAdd
format.html { redirect_to #group, notice: 'De begeleider is toegevoegd.' }
end
end
end
end
end
The if !email.blank? => false and if userToAdd.blank? => true branches don't render or redirect. Therefor rails if looking for a template with the name of that action.
I'd suggest to enhance you routing editing routes.rb:
resources :groups do
member do
get 'new_caretaker'
post 'create_caretaker'
end
end
And then you call this redirect:
redirect_to group_new_caretaker_path(#group)
in the controller when needed.
This way, you'll also get a params[:group_id] param in the new_caretaker controller method to be able to handle group-related data (if needed).
The fix: I did not render anything in the if, just in the else. Some good tips in the answers though.

Create Model from unrelated View on Rails 3.0

I'm trying to do something fairly simple but I'm not sure the rails way to do it. At its simplest, I have an index page where you can sign up for a mailing list.
I'm trying to set it up so that you can add yourself to the mailing list from the index page without ever seeing the mailing list views. I can submit the data properly using something like:
= form_for #mailing_list, :remote => true do |form|
= if #mailing_list.errors.any?
%ul
= #mailing_list.errors.full_messages.each do |message|
%li
= message
.field
= form.label :email, 'Your email'
= form.text_field :email
= form.submit "Add to Mailing List"
With the controller:
def create
#mailing_list = MailingList.new(params[:mailing_list])
if #mailing_list.save
redirect_to(:root, :notice => 'Mailing list was successfully created.')
else
? How do I return the errors ?
end
end
But I am unable to get the errors back (ie. Email not valid, etc.). Is there a better way to do what I'm attempting? I would just like to be able to call and respond to actions of the MailingList controller from the index page view...
I believe that you are wanting a form that will add someone to a Mailing list without leaving that page.
Better? Hmmm.. Well, I'll tell you what I do and you can decide what you like.
I would use respond_to in the controller to differentiate between the standard html call and the remote js call. Then, I would handle the page changes in the view. I like keeping the display in the views.
Controller:
def create
#mailing_list = MailingList.new(params[:mailing_list])
if #mailing_list.save
respond_to do |format|
format.html { redirect_to(:root, :notice => 'Mailing list was successfully created.') }
format.js { render }
end
else
respond_to do |format|
format.html { render }
format.js { render :errors }
end
end
end
create.js.erb
$('#errors').html('').hide();
$('form').html('Mailing list was successfully created.'); // needs a better element
errors.js.erb
$('#errors').html('<%= escape_javascript(#mailing_list.errors.full_messages.collect { |msg| content_tag :li, msg }.join().html_safe) %>').show();
You can do something with the errors object on #mailing_list, e.g.
flash.now[:error] = #mailing_list.errors.full_messages

Resources