I have an update form where I want a user to update a date. The date should always be NEWER than the date which is currently in the database so I want my script to validate it after the user hits the submit button.
I've come this far:
<%= simple_form_for(calendar, :html => { :method => :put, :name => 'extend_link' }) do |f| %>
<p>Select the new date (must be newer than the current date): <%= f.date_select :end_at %> at <%= f.time_select :end_at, { :ignore_date => true } %></p>
<% end %>
standard update put in my controller, updating the calendar model
def update
#calendar = current_user.calendar.find(params[:id])
respond_to do |format|
if #calendar.update_attributes(params[:calendar])
format.html { redirect_to calendar_path, notice: 'The end date was extended.' }
format.json { head :no_content }
end
end
end
I've checked the source after the form was rendered to understand how the date and time select works and also after researching a lot it is clear that the date is being split into different pieces before it is "merged" into my model and the end_at column
calendar[end_at(3i)]
calendar[end_at(2i)]
....
but for some reason, I cannot access the complete params[:end_at] after the form was submitted. However, it mus be accessible as otherwise how could the model get updated in one piece? I went nuts with this.
It could be so easy:
if params[:end_at] < #calendar.end_at
puts "The new ending date is not after the current ending date."
else
#calendar.update_attributes(params[:calendar])
end
why does it not work and how I can solve my problem?
Thanks for any help.
You can do this in your controller, but it sounds like this is a model validation, so I'd put it there. Use the magic of ActiveModel::Dirty to find the attribute before and after, perhaps something like this:
class Calendar < ActiveRecord::base
validate :date_moved_to_future
private
def date_moved_to_future
self.errors.add(:end_at, "must be after the current end at") if self.end_at_changed? && self.end_at_was < self.end_at
end
end
Related
I’m using Rails 4.2.7. I would like to throw a validation error if a user doesn’t enter their date of birth field in the proper format, so I have
def update
#user = current_user
begin
#user.dob = Date.strptime(params[:user][:dob], '%m/%d/%Y')
rescue ArgumentError => ex
end
if #user.update_attributes(user_params)
and I have this in my view
<%= f.text_field :dob, :value => (f.object.dob.strftime('%m/%d/%Y') if f.object.dob), :size => "20", :class => 'textField', placeholder: 'MM/DD/YYYY' %>
<% if #user.errors[:dob] %><%= #user.errors[:dob] %><% end %>
However, even if someone enters a date like “01-01/1985”, the above doesn’t return a validation error to the view. What do I need to do to get the validation error to be returned properly?
Edit: Per one of the answers given, I tried
#user = current_user
begin
#user.dob = Date.strptime(params[:user][:dob], '%m/%d/%Y')
rescue ArgumentError => ex
puts "Setting error."
#user.errors.add(:dob, 'The birth date is not in the right format.')
end
if #user.update_attributes(user_params)
last_page_visited = session[:last_page_visited]
if !last_page_visited.nil?
session.delete(:last_page_visited)
else
flash[:success] = "Profile updated"
end
redirect_to !last_page_visited.nil? ? last_page_visited : url_for(:controller => 'races', :action => 'index') and return
else
render 'edit'
end
And even though I can see the "rescue" branch called, I'm not directed to my "render 'edit'" block.
Triggering an exception doesn't add anything to the errors list. If you just want to tweak this code slightly, you should be able to call errors.add inside the rescue block. Something like #user.errors.add(:dob, 'some message here').
Keep in mind that this will only validate the date of birth when using this controller method. If you want to validate the date of birth whenever the user is saved, you'll want to explicitly add the validation to the model. You can write your own custom validation class or method, and there are also some gems that add date validation.
Calling update_attributes clears out the errors that you set in the rescue. You should check for errors, and if none, then continue on, something like this:
#user = current_user
begin
#user.dob = Date.strptime(params[:user][:dob], '%m/%d/%Y')
rescue ArgumentError => ex
puts "Setting error."
#user.errors.add(:dob, 'The birth date is not in the right format.')
end
if !#user.errors.any? && #user.update_attributes(user_params)
last_page_visited = session[:last_page_visited]
if !last_page_visited.nil?
session.delete(:last_page_visited)
else
flash[:success] = "Profile updated"
end
redirect_to !last_page_visited.nil? ? last_page_visited : url_for(:controller => 'races', :action => 'index') and return
end
render 'edit'
Since you redirect_to ... and return you can close out the conditional and, if you make it this far, simply render the edit page.
You may also want to add a simple validation to your user model:
validates :dob, presence: true
This will always fail if the dob can't be set for some other, unforseen, reason.
To get the user entered string to populate the field on re-load, you could add an accessor to the user model for :dob_string
attr_accessor :dob_string
def dob_string
dob.to_s
#dob_string || dob.strftime('%m/%d/%Y')
end
def dob_string=(dob_s)
#dob_string = dob_s
date = Date.strptime(dob_s, '%m/%d/%Y')
self.dob = date
rescue ArgumentError
puts "DOB format error"
errors.add(:dob, 'The birth date is not in the correct format')
end
Then change the form to set the :dob_string
<%= form_for #user do |f| %>
<%= f.text_field :dob_string, :value => f.object.dob_string , :size => "20", :class => 'textField', placeholder: 'MM/DD/YYYY' %>
<% if #user.errors[:dob] %><%= #user.errors[:dob] %><% end %>
<%= f.submit %>
<% end %>
And update the controller to set the dob_string:
def update
#user = User.first
begin
##user.dob = Date.strptime(params[:user][:dob], '%m/%d/%Y')
#user.dob_string = user_params[:dob_string]
end
if ! #user.errors.any? && #user.update_attributes(user_params)
redirect_to url_for(:controller => 'users', :action => 'show') and return
end
render 'edit'
end
def user_params
params.require(:user).permit(:name, :dob_string)
end
I would add a validation rule in the model. Like:
validates_format_of :my_date, with: /\A\d{2}\/\d{2}\/\d{4}\z/, message: 'Invalid format'
Try adding validation rule in model.
validate :validate_date
def validate_date
begin
self.dob = Date.parse(self.dob)
rescue
errors.add(:dob, 'Date does not exists. Please insert valid date')
end
end
and in your controller update your code
...
#user.update_attributes(user_params)
if #user.save
....
I think this is a case where Active Model shines. I like to use it to implement form objects without extra dependencies. I don't know the exact details of your situation but below I pasted a small demo that you should be able to adapt to your case.
The biggest benefit is that you don't pollute your controllers or models with methods to support profile updates. They can be extracted into a separate model which simplifies things.
Step 1: Store dob in users
Your users table should have a column dob of type date. For example:
class CreateUsers < ActiveRecord::Migration
def change
create_table :users do |t|
t.string :name, null: false
t.date :dob, null: false
end
end
end
Don't put anything fancy in your model:
class User < ActiveRecord::Base
end
Step 2: Add Profile
Put the following in app/models/profile.rb. See comments for explanations.:
class Profile
# This is an ActiveModel model.
include ActiveModel::Model
# Define accessors for fields you want to use in your HTML form.
attr_accessor :dob_string
# Use the validatiors API to define the validators you want.
validates :dob_string, presence: true
validate :dob_format
# We store the format in a constant to keep the code DRY.
DOB_FORMAT = '%m/%d/%Y'
# We store the user this form pertains to and initialize the DOB string
# to the one based on the DOB of the user.
def initialize(user)
# We *require* the user to be persisted to the database.
fail unless user.persisted?
#user = user
#dob_string = user.dob.strftime(DOB_FORMAT)
end
# This method triggers validations and updates the user if validations are
# good.
def update(params)
# First, update the model fields based on the params.
#dob_string = params[:dob_string]
# Second, trigger validations and quit if they fail.
return nil if invalid?
# Third, update the model if validations are good.
#user.update!(dob: dob)
end
# #id and #persisted? are required to make form_for submit the form to
# #update instead of #create.
def id
#user.id
end
def persisted?
true
end
private
# Parse dob_string and store the result in #dob.
def dob
#dob ||= Date.strptime(dob_string, DOB_FORMAT)
end
# This is our custom validator that calls the method above to parse dob_string
# provided via the params to #update.
def dob_format
dob
rescue ArgumentError
errors[:dob] << "is not a valid date of the form mm/dd/yyyy"
end
end
Step 3: Use the form in the controller
Use Profile in ProfilesController:
class ProfilesController < ApplicationController
def edit
# Ensure #profile is set.
profile
end
def update
# Update the profile with data sent via params[:profile].
unless profile.update(params[:profile])
# If the update isn't successful display the edit form again.
render 'edit'
return
end
# If the update is successful redirect anywhere you want (I chose the
# profile form for demonstration purposes).
redirect_to edit_profile_path(profile)
end
private
def profile
#profile ||= Profile.new(user)
end
def user
#user ||= User.find(params[:id])
end
end
Step 4: Render the form with form_for
In app/views/profiles/edit.html.erb use form_for to display the form:
<%= form_for(#form) do |f| %>
<%= f.label :dob_string, 'Date of birth:' %>
<%= f.text_field :dob_string %>
<%= f.submit 'Update' %>
<% end %>
Step 5: Add routing
Keep in mind to add routing to config/routes.rb:
Rails.application.routes.draw do
resources :profiles
end
That's it!
I am currently trying to make custom validations work with an input of dates, but, unfortunately, it doesn't seem to work.
There are two pages inside the application, Index page and Search page. Inside the index page there is a text field that takes in a date. I am using Chronic gem which parses text into dates. If the date is invalid, Chronic returns nil. If it is valid, it redirects to search page and shows the date.
The code I wrote so far doesn't seem to work properly, but what I want to achieve is..
1) to validate that Chronic doesn't return nil
2) to validate that date is greater than today's date
Please note that I am not using a database with this, I just want to be able to validate inputted date without ActiveRecord. If someone could help me with this, your help will be greatly appreciated.
views/main/index.html.erb
<%= form_tag({controller: "main", action: "search"}, method: "get") do %>
<%= label_tag(:q, "Enter Date:") %>
<%= text_field_tag(:q) %>
<%= submit_tag "Explore", name: nil %>
<% end %>
views/main/search.html.erb
<%= #show_date %>
main_controller.rb
def search
passed_info = Main.new
if passed_info.valid_date?
#show_date = passed_info
else
flash[:error] = "Please enter correct date!"
render :index => 'new'
end
end
models/main.rb
class Main
include ActiveModel::Validations
include ActiveModel::Conversion
extend ActiveModel::Naming
attr_accessor :q
validates_presence_of :q
def initialize(params={})
params.each do |attr, value|
self.public_send("#{attr}=", value)
end if params
end
def persisted?
false
end
def valid_date?
require 'chronic'
if Chronic.parse(q).nil? || Chronic.parse(q) < Time.today
errors.add(:q, "is missing or invalid")
end
end
end
EDIT:
this is what goes wrong...
localhost:3000
then it redirects to ..
localhost:3000/main/search?utf8=%E2%9C%93&q=invalid+date+test
No validation, no date, nothing..
The Problem
Be more careful with return values. When you try to guard your controller with if valid_date?, what you're doing is checking to see if valid_date? returns false. If the parse fails, the return value is the output of errors.add, which in turn is the output of Array#<<. Relevantly, the output isn't nil or false, so it evaluates to true, thus the if clause passes and you move forward.
Potential Solution
You probably want to let the Rails Validation Framework do more work for you. Instead of treating valid_date? as a public method which the controller calls, call the valid? method that gets added by ActiveModel::Validations. valid? will return a boolean, based on whether all the model validations pass. Thus, you would, as is the Rails Way, call if model_instance.valid? in your controller.
This lets you just write validator methods in your model which express the logic you're trying to write. Right now, you have all validation logic for dates in a single method, with a single error message. Instead, you could put two methods, which add more descriptive individual error methods.
class YourClass
include ActiveModel::Validations
validate :date_is_valid
validate :date_not_before_today
private
def date_is_valid
if Chronic.parse(q).nil?
errors.add(:q, "Date is invalid")
end
end
def date_not_before_today
if Chronic.parse(q) < Date.today
errors.add(:q, "Date cannot be before today")
end
end
end
Just as correctly suggested by ABMagil, I would like to post the full solution to my answer. In fact, this answer can apply really to anyone who wants to use validations using ActiveModel, with or without Chronic gem or dates involved. It can act as a valid template so to speak.
Frankly, most of my mistakes came from a really poor, at the time, understanding of what I actually tried to achieve. Most of the code needed major refactoring, see below the updates that I had to make. I tried to keep the code as well documented as possible.
Solution:
views/main/index.html.erb
<%= form_for #search, url: { action: "search" },
html: { method: :get } do |f| %>
# Displays error messages if there are any.
<% if #search.errors.any? %>
The form contains <%= pluralize(#search.errors.count, "error") %>.<br />
<% #search.errors.each do |attr, msg| %>
<%= msg %><br />
<% end %>
<% end %>
<%= f.label :q, "Enter Date:" %>
<%= f.text_field :q %>
<%= f.submit "Explore", :class => 'submit' %>
<% end %>
views/main/search.html.erb - same as before
<%= #show_date %>
main_controller.rb
def index
# Initializes a string from the form to a variable.
#search = Search.new
end
def search
# Retrieves the input from the form.
#search = Search.new(params[:search])
# Checks for validity,
# If valid, converts a valid string into a date.
# Redirects to search.html.erb
# If not valid, renders a new index.html.erb template.
if #search.valid?
#show_date = (Chronic.parse(params[:search][:q])).to_date
else
render :action => 'index'
end
end
models/main.rb
class Main
include ActiveModel::Validations
include ActiveModel::Conversion
extend ActiveModel::Naming
# Accepts the passed attribute from the form.
attr_accessor :q
# If form is submitted blank, then output the prompting message.
validates_presence_of :q, :message => "This text field can't be blank!"
# Two custom validations that check if passed string converts to a valid date.
validate :date_is_valid
validate :date_not_before_today
# Initializes the attributes from the form.
def initialize(attributes = {})
attributes.each do |name, value|
send("#{name}=", value)
end
end
# Checks for persistence, i.e. if it's a new record and it wasn't destroyed.
# Otherwise returns false.
def persisted?
false
end
# ABMagil's code used for custom date validations
private
require 'chronic'
def date_is_valid
if Chronic.parse(q).nil?
errors.add(:base, "Date is invalid")
end
end
def date_not_before_today
if !Chronic.parse(q).nil?
if Chronic.parse(q) < Date.today
errors.add(:base, "Date cannot be before today")
end
end
end
end
Result:
I am trying to import four data fields. Child_id, First_name, Last_name, Medical. In my form it is only pulling in child_id:
<%= form_for #check_in, :url => {:controller => 'check_ins', :action => 'create' } do |f| %>
<% #account.children.each do |child| %>
<%= f.check_box :child_id, {:checked => 'checked', :multiple => true}, child.id.to_s %>
<%= image_tag child.photo.url(:thumb) %>
<span class="name"><%= child.first %>
<%= child.last %></span><br/>
<% end %>
<% end %>
Model associations:
class CheckIn < ActiveRecord::Base
has_many :children
end
class Child < ActiveRecord::Base
belongs_to :account
belongs_to :check_in
end
This is my create method in my check_ins controller.
def create
#check_in = CheckIn.new(params[:check_in])
begin
params[:check_in] [:child_id].each do |child_id|
unless child_id == 0.to_s
CheckIn.new(:child_id => child_id).save!
end
end
respond_to do |format|
format.html { redirect_to(:controller => 'sessions', :action => 'new') }
format.json { render json: #check_in, status: :created, location: #check_in }
end
rescue
respond_to do |format|
format.html { render action: "new" }
format.json { render json: #check_in.errors, status: :unprocessable_entity }
end
end
end
This form is also on a show page. The checkbox is there and next to the checkbox is the information pulled from another table: child.first, child.last. But those fields I want to be selected along with the checkbox like the child_id is.
Right now I have a child saved in my table with an id of 8 it would pull in the 8 but the fields for child.first and child.last aren't pulling into the new table that the id is.
Hm, by "import data field" you mean showing the attributes of child within your form?
The form looks ok to me, now it depends on things outside of this form.
I would check on the following:
Are the fields of child indeed named first, last and photo as used in
the code snippet, and opposed to those you listed in your question?
What are the contents of #account and #account.children? You could output both on your page to check.
I only see one form tag in your form block: f.check_box :child_id. The other stuff like <%=
child.first %> are not part of the form, even though they're within the form block.
EDIT:
There are a number of problems. First, going strictly by the way the associations are set up, CheckIn should not have child_id attribute. It has_many :children while Child belongs_to :check_in. CheckIn should not have a child_id, Child should have a check_in_id. Therefore you should be updating each selected child with a check_in_id value. I'd read up on ActiveRecord associations: http://guides.rubyonrails.org/association_basics.html
Second, the way the form is rendering controls I think you're ending up with multiple checkboxes with the same name. When rails assembles the params hash it's going to ignore all but the last hash item with a particular key. So even if everything else was set up correctly you'd still only save one child to a check in. I'd watch this tutorial on nested attributes:
http://railscasts.com/episodes/196-nested-model-form-part-1?view=asciicast
Lastly, I don't understand what you mean when you say it's not saving child.first and child.last (aka first_name and last_name?). That information is stored in the child object already, correct? Why would that be saved elsewhere?
If all of this was working correctly you'd be able to do things like this:
# find an account
account = Account.find(99)
# create a check_in
check_in = CheckIn.create
# save one or more (or all) of account's children to the check_in
check_in.children << account.children
# see the first name of a child associated with a check_in
check_in.children[0].first
OK guys this one has had me frustrated for a few hours now. I've searched and can't seem to find anyone else having a similar issue. Any help would be much appreciated.
I am trying to make an AJAX call to my controller to update a line_item's quantity property. The code is as follows:
###Controller update action###
def update
#line_item = LineItem.find(params[:id])
#quote = #line_item.quote
respond_with(#line_item, #quote) do |format|
if #line_item.update_attributes(params[:line_item])
format.html { redirect_to(#quote,
:notice => 'Quantity was successfully updated.')}
format.js
else
format.html { redirect_to(#quote,
:notice => 'Oops! James messed up again.')}
format.js { render 'shared/update_error' }
end
end
end
#####View code######
- #quote.line_items.each do |item|
= form_for item, :remote => true do |f|
%tr
%td.quantity_cell
= f.text_field :quantity, :size => 2
= image_submit_tag("update_quantity_button.png", :id => "quantity_update")
####Javascript for the callback####
$("#quote_show_parts").html("<%= escape_javascript(render 'shared/show_quote') %>");
The problem is that the first request will work as expected. the call is executed, record updated, and the change reflected on the page. Checking the page source, you can see the "_method ... 'put'" hidden tag in the form.
if i change the value in the field and hit the button again, i get a routing error:
Started POST "/line_items/90" for 127.0.0.1 at 2011-12-08 15:11:45 -0500
ActionController::RoutingError (No route matches [POST] "/line_items/90"):
Now, it seems as though the _method tag (still there when checking the page source) is being ignored and a POST call being made. Either way a POST route is defined for that URL so i'm not sure why the "no route" error would happen. if i remove the ':remote => true' the functionality works as expected and there are no issues.
Thank you all very much is advance.
EDIT: added model code:
class LineItem < ActiveRecord::Base
belongs_to :part
belongs_to :quote
def total_price(company)
part.price(company) * quantity
end
end
After validation, I got an error and I got returned back to :action => :new.
Some field on form already filled, so I want to keep them filled even after error message too.
How it can be done?
Your View (new.html.erb) something like following
<%= error_message_for :user %>
<% form_for :user, :action=>"create" do|f|%>
<%= f.text_field :login %>
<% end %>
Controller Code (create method)
def create
#user=User.new(params[:user])
if #user.save
redirect_to :action=>'index'
else
render :action=>'new' #you should render to fill fields after error message
end
end
Since in my case the form was in the view of another controller I did use flash to store my data and then check if there is data in flash present. If yes take this for default values for your input fields, if not just show whatever you want to show then.
So snippets from my code
flash[:date] = start_date
# in the view where to form resides
start_day = flash[:date].nil? nil : flash[:date].day
# ...
<%= select day start_day ... %>
Hope that helps some of you ;-).