Showstopper - can't save a record

Hi forummates,

I’m relatively new to Ruby and Rails and have been creating a prototype
Rails app to mimic an existing desktop application. I’ve tried several
tutorials and worked my way through Agile Web D. with Rails.
But there’s a seemingly simple event in the prototype that’s stopped me
in my tracks. I can’t save a record, and can’t figure out why!

Here’s the situation:

I have borrowers, loans, and payments tables (a borrower has_many loans,
a loan has_many payments through loans). I can add a new borrower, which
I do by giving the user a form in which to fill in the SSN, FirstName,
LastName of the borrower. They press a Save link which successfully
mimics the Submit button (thanks to another post to this forum), after
which they are taken to another form in which they can edit the rest of
the borrower record.

But the loan and payment situations are more complex. In both cases my
system generates some of the attributes (field values) that will go in
the record, and gets the rest via a form the user fills out. For example
when adding a loan, the system knows the borrower_id and supplies it
automatically. Adding a loan works okay now too.

My big problem comes when attempting to add a payment record. One of the
required fields (validates_presence_of) is TransDate (transaction date).
Even if the user fills that in, they get an error message: TransDate is
missing, and indeed it now appears blank on the screen.

I was hoping that learning how to do testing in Rails would help me
resolve this problem, but it’s only worse! I’ve had problems with the
test_create results in all three controllers. However, I’ve resolved the
issues with the borrowers and loans controllers - they had to do with
initializing required fields in the tests to simulate the user filling
in those fields in the real app. But this doesn’t seem to work with the
TransDate field in the payments_controller_test. For kicks, I tried
assigning @payment.TransDate = Date.today in the payment controller and
indeed, that puts today’s date into the record and saves it
successfully. However that same technique fails in the test:
post :create, :payment => { :PmtPeriodNum => 5, :TransDate => Date.today
}

So my problem is:

  1. In the real app, when a user creates a new payments record including
    entering a TransDate, they get the error message “Transdate is missing”
    and the date they entered disappears.
  2. In the payments_controller_test I can’t figure out how to read what’s
    in @payments. That is, a breakpoint set in the payments_controller
    itself gets a @payment record with a nil TransDate (even when I set it
    to Date.today in the test), but a breakpoint set in the
    payments_controller_test always gets @payment = nil. In the wording of
    the tests, symbols are used instead of instance variables and of course
    the value of :payment is “:payment”.

On the data side of things, I compared the definition of the TransDate
field in the payments Mysql table with other date fields. TransDate is
defined differently in that it is not allowed to be null. If I leave the
default value blank, Mysql converts it to 0000-00-00. If I change the
default value to a valid date like 0001-01-01, the test and the app run
fine, but the app displays the phony default date in the form, where I
DON’T want it. So I changed the definition of TransDate in the test
table but that alone made no difference in my result. I also tried
assigning Date.today to one of the date fields in the loans table and
that test works fine.

So is this a problem with Rails reading Mysql tables? Or I am just going
about the coding or setup all wrong? I’m new to Ruby/Rails although I’ve
been programming for over thirty years and learn new stuff all the time.
I’ve tried googling this and although lots of ruby/mysql/null dates
problems show up, I haven’t been able to find one like mine. In the
test.log, the create action renders the test TransDate correctly as a
date version of today. In an irb session inside a breakpoint, Ruby
correctly assigns the date as well. But the test always fails, as does
the actual app form.

I tried removing the model’s validates_presence_of :TransDate and
changing the Mysql field to allow nulls. Only if I do both of those will
the test and the app run. But that puts me back to coding data
requirements in procedural code rather than in the table or model. Not
very DRY at all!

Now, whoever read this far gets a prize (and my amazed appreciation).
I’ll be even more appreciative if you can help me solve my problem!

Gratefully,
Shauna

On 10/24/06, Shauna [email protected] wrote:

My big problem comes when attempting to add a payment record. One of the
required fields (validates_presence_of) is TransDate (transaction date).
Even if the user fills that in, they get an error message: TransDate is
missing, and indeed it now appears blank on the screen.

What control are you using for the date entry? Unless you are using
the rails-supplied date_select, there will be some problems,
especially with non-US date formats. What date format are you
expecting? Anyway, if you are using text_field for date entry, if the
date cannot be parsed properly, Date.parse or DateTime.parse (can’t
remember which one is used by rails) will silently return nil, which
may explain the error you are seeing.

Cheers,
Max

Hi Max,

Thanks for your reply. I am using a modified version of the
simple_date_select plugin for dates. So I tried moving that plugin out
and going with the rails-supplied date_select. The results are identical
(in both the app itself and the test).

Any other suggestions? If not, I’ll file a bug report.

Shauna

On 10/25/06, Shauna [email protected] wrote:

Shauna

One other thing I noticed is that your field names don’t follow the
rails naming conventions. Theoretically, this should not be related to
your problem, but try renaming the TransDate field to trans_date and
try that again.

Max

Hey Max,

Thanks for sticking with this. I changed TransDate to trans_date in all
occurrences, refired Rails, and got exactly the same failure in both the
payments_controller_test and the real app.

I know the payments_controller create method is going through the rescue
code because it picks up the following message from the Exception
object:
Mysql::Error: #23000Column ‘trans_date’ cannot be null: INSERT INTO…

As I said, that column is not left null on the form when it is
submitted.

Looks like I’ll have to either move the validation into procedural code
or give up on Rails until this is fixed.

Shauna

Hi Shauna,

Shauna wrote:

I know the payments_controller create method
is going through the rescue code because it picks
up the following message from the Exception
object:
Mysql::Error: #23000Column ‘trans_date’ cannot
be null: INSERT INTO…

As I said, that column is not left null on the form
when it is submitted.

When you say “not left null on the form” are you saying you’ve checked
the
value of the params hash when the form’s submitted to the controller?
Or
just that the form has had a value entered?

Best regards,
Bill

Hi Bill,

Both.

I put in a breakpoint and checked the hash and it includes
“trans_date”=>nil. Also, if I manually enter a date (or even accept
Rails’ default of today’s date) the hash and the message are the same.
When the screen redisplays (with my flash notice at the top including
the Mysql::Error message), the trans_date selectors pop back to their
default values.

If I remove both the validates_presence_of and the Mysql “not null”, the
test is successful (even if I leave in the Msql default date of
0000-00-00). However, the hash still reads “trans_date”=>nil and a nil
is inserted for trans_date (null in the Mysql table).

Shauna

Shauna,

It sounds like the form is not sending the information to the
controller. Can you paste the code of the date selection input (ruby
and html)?

The trans_date will not make it to the controller if it is named
incorrectly (both ObjectName and Method).

If you remove validate_presence, and the record is created with the
default 0000-00-00, then that also shows that the trans_date is not
making it over.

On 10/25/06, Shauna [email protected] wrote:

default values.

If I remove both the validates_presence_of and the Mysql “not null”, the
test is successful (even if I leave in the Msql default date of
0000-00-00). However, the hash still reads “trans_date”=>nil and a nil
is inserted for trans_date (null in the Mysql table).

Shauna

What is the html code generated for the date_select?

Max

Hi Max and Ryan,

Before I replied I tried one more thing - mysqldump of the payments
table and reloading it with no change (just in case mysql got all balled
up with the repeated table structure changes which I did through
PHPMyAdmin). But there was no difference - the default value gets loaded
into trans_date and never gets changed. Also, no other field values
entered in the post are taking. So I must have screwed up something in
my code during all these changes. Anyway, here it is.

Generated html (I assume by this you mean what I capture from the View
Page Source):

  • Date:

    payments_controller.rb:
    def new
    @borrower = Borrower.find(:first, :conditions => "id = " +
    session[:borrowerid].to_s )
    @loan = Loan.find(:first, :conditions => "id = " +
    session[:loanid].to_s )
    @payment = Payment.new
    @payment.loan_id = session[:loanid]

    # determine PmtPeriodNum based on previous transaction and new/same 
    

    option
    if session[:pmtper_new_or_same] == ‘new’
    if @loan.payments.size < 1
    @payment.PmtPeriodNum = 0

    session[:pmtpernum] = 0

      else
        priorperiod = @loan.payments.find(:first, :order => 
    

    “PmtPeriodNum DESC”)
    @payment.PmtPeriodNum = priorperiod.PmtPeriodNum.floor + 1

    session[:pmtpernum] = priorperiod.PmtPeriodNum.floor + 1

      end
    elsif session[:pmtper_new_or_same] == 'same'
      if @loan.payments.size < 1
        flash[:notice] = 'This is the first transaction for this loan. ' 
    
    •     'Use the NEW PERIOD button rather than the SAME PERIOD 
      

    button.’
    redirect_to :action => ‘list’
    else
    priorperiod = @loan.payments.find(:first, :order =>
    “PmtPeriodNum DESC”)
    nuDiff = priorperiod.PmtPeriodNum -
    priorperiod.PmtPeriodNum.floor
    if nuDiff >= 0.89
    @payment.PmtPeriodNum = priorperiod.PmtPeriodNum + 0.001

    session[:pmtpernum] = priorperiod.PmtPeriodNum + 0.001

        else
          @payment.PmtPeriodNum = priorperiod.PmtPeriodNum + 0.1
    

    session[:pmtpernum] = priorperiod.PmtPeriodNum + 0.1

        end
      end
    else
      flash[:notice] = 'Error with New/Same. Please contact DownHome 
    

    Solutions.’
    redirect_to :action => ‘list’
    end
    session[:pmtpernum] = @payment.PmtPeriodNum
    end

    def create
    @borrower = Borrower.find(:first, :conditions => "id = " +
    session[:borrowerid].to_s )
    @loan = Loan.find(:first, :conditions => "id = " +
    session[:loanid].to_s )
    begin
    @payment = Payment.new(params[:id])
    @payment.loan_id = session[:loanid]
    @payment.PmtPeriodNum = session[:pmtpernum]

    @payment.trans_date = Date.today # this test works (record

    posts with today’s date)
    if @payment.save
    flash[:notice] = ‘Payment record posted.’
    redirect_to :action => ‘list’
    else
    @loans = Loan.find_all
    flash[:notice] = “Item could not be saved.”
    render :action => ‘new’
    end
    rescue Exception => exc
    flash[:notice] = “Item could not be saved: #{exc.message}”
    redirect_to :action => ‘new’
    end
    breakpoint
    end

    views/payments/new.html:

    New payment transaction for Loan <%= @loan.LoanID %>

    Borrower: <%= @borrower.FirstName %> <%= @borrower.LastName %>

    <%= link_to_function “Save”, “document.forms[0].submit()” %> |
    <%= link_to ‘Cancel’, :action => ‘list’ %> /
    <%= link_to ‘List of all transactions’, :action => ‘list’ %>

    <%= start_form_tag :action => ‘create’ %>

    <%= render :partial => 'form' %>
    <%= end_form_tag %>

    views/payments/_form.html:
    <%= error_messages_for ‘payment’ %>

    • Period: <%= text_field 'payment', 'PmtPeriodNum', 'size' => 3 %>
    • Date: <%= date_select 'payment', 'trans_date' %>
    • Code: <%= text_field 'payment', 'TransCode', 'size' => 3 %>
    • Pmt Recd: <%= text_field 'payment', number_to_currency('AmtPaid'), 'size' => 10 %>

    Beginbal
    <%= text_field 'payment', 'BeginBal' %>

    Thanks again for helping!

    I’ll be leaving tomorrow morning for a two week trip so won’t be able to
    look at this in depth for quite a while. And maybe that’s a Good
    Thing…

    Shauna

  • Long wrote:

    I don’t see a @payment.attributes = params[:payment] here so that the
    object is
    updated with the form attributes.

    HTH,

    Long
    www.edgesoft.ca

    Long, that fixed the logjam! Now when I get back from my trip I’ll back
    out of my changes one by one till I’m back at TransDate with the proper
    formatting, and see if anything breaks along the way.

    I’m not quite clear on the differences between
    @payment = Payment.new
    @payment = Payment.new(params[:id])
    @payment.attributes = params[:payment]
    @payment = Payment.new(params[:payment])

    and when you should use each of them. Is there a clear explanation
    somewhere? I would like to use the most succinct method(s) possible, of
    course.

    Thanks, and Long, Max and Ryan, you are my heroes!
    Shauna

    Shauna wrote:

    Page Source):

  • Date:
  • Curious. What is ‘trans_date(1i)’ ? I have not seen this in so far.
    Usually it would
    have been just ‘payment[trans_date]’.

    begin
      @payment = Payment.new(params[:id])
      @payment.loan_id = session[:loanid]
      @payment.PmtPeriodNum = session[:pmtpernum]
    

    @payment.trans_date = Date.today # this test works (record

    posts with today’s date)

    I don’t see a @payment.attributes = params[:payment] here so that the
    object is
    updated with the form attributes.

      if @payment.save
    

    HTH,

    Long
    www.edgesoft.ca

    Shauna wrote:

    Long, that fixed the logjam! Now when I get back from my trip I’ll back
    out of my changes one by one till I’m back at TransDate with the proper
    formatting, and see if anything breaks along the way.

    I’m not quite clear on the differences between
    @payment = Payment.new
    Gives you an uninitialized instance

    @payment = Payment.new(params[:id])

    Gives you another uninitialized instance…I think.

    @payment.attributes = params[:payment]
    Updates @payment ‘fields’ with corresponding ‘values’ in the params hash

    @payment = Payment.new(params[:payment])
    Gives you a new and initialized (partial?) instance

    Someone can correct if I am mistaken… I think knowing when to use each
    just depends on the need and convenient.

    and when you should use each of them. Is there a clear explanation
    somewhere? I would like to use the most succinct method(s) possible, of
    course.

    Thanks, and Long, Max and Ryan, you are my heroes!

    Hmm, I think Max and Ryan really deserve more credit.

    Go and enjoy your time off!

    Cheers,

    Long
    www.edgesoft.ca

    On 10/25/06, Shauna [email protected] wrote:

    Long, that fixed the logjam! Now when I get back from my trip I’ll back
    out of my changes one by one till I’m back at TransDate with the proper
    formatting, and see if anything breaks along the way.

    Also, it’s a good idea to include whatever code you think could be
    related the problem. We’ll be able to help you more quickly, as it’s
    far more likely to be an error in your code than a bug in Rails itself
    :slight_smile:

    Pat

    On 10/25/06, Shauna [email protected] wrote:

    I’m not quite clear on the differences between
    @payment = Payment.new
    This creates an empty Payment object

    @payment = Payment.new(params[:id])
    This should give you an error…

    @payment.attributes = params[:payment]
    This takes an existing payment object (maybe initialized as your first
    line) and sets all its attributes to the corresponding values in the
    hash

    @payment = Payment.new(params[:payment])
    This creates a new payment object with its attributes initialized to
    the corresponding values in the hash

    The easiest is just to do
    @payment = Payment.new params[:payment]

    The problem with your code was that you were doing
    Payment.new(params[:id]). That’s similar to calling something like
    Payment.new(5) which doesn’t really make sense.

    Pat

    I responded earlier, but realized that my response may not have been
    quite detailed/nitpicky enough. So I’m going to nitpick your
    response, if that’s okay :slight_smile:

    On 10/25/06, Long [email protected] wrote:

    I’m not quite clear on the differences between
    @payment = Payment.new
    Gives you an uninitialized instance
    Gives you an instance with all its attributes initialized to default
    value.

    @payment = Payment.new(params[:payment])
    Gives you a new and initialized (partial?) instance
    Shortcut for Payment.new and Payment.attributes=. What I mean is that
    you’ll get an instance with attributes initialized to their default
    values, and then any attributes in the hash will be assigned. So for
    example, with a Person model that has name and age attributes…

    hash = { :name => “John”, :age => 30 }
    Person.new(hash) # :name => John, :age => 30

    hash = { :name => “John” }
    Person.new(hash) # :name => John, :age => nil

    If you have defaults specified in your db, they’d be initialized to
    that instead of nil.

    hth

    Pat

    Thanks, Long and Pat. I think I understand now. And being nitpicky is
    good good good, the multiple explanations and extra details gave me a
    more complete picture.

    What happens with us newbies, is we copy things from multiple places,
    anything that looks like we could use it, without having a solid feel
    for how all the pieces should fit together yet. So sometimes crucial
    bits get left out (maybe a piece is needed for one technique and not for
    another). This is the first time I’ve seen .attributes, and now it makes
    perfect sense as an extra step if I used .new(params[:id]) instead of
    .new(params[:payment]).

    By careful placement of .attributes, I can allow the user to change some
    things via the form and not others. Nifty.

    Shauna