Topic: Single string in models needs to be multiple lines in form

Hi,

I have the following problem: I have a License model, being license information for a user. In this mode is a registration data field. It is a single VARCHAR(100), but actually it is being displayed in the end-user application as four lines 25 characters each. In the database this regdata is modeled as a single attribute in order to ensure uniqueness of the registration data in an easy way.

I want to be able to edit the registration data on my textfield in four single textfields. The data of these fields will be, after submitting the form, padded with spaces so that they're all 25 characters long and will then by concatted into the single string in the model.

But I'm having problems to create the four temp strings and edit them via the form. I don't know which method I have to use for the strings.. I keep getting an "method unknown" or "wrong number of arguments" error.

Here's what I have so far:

  def edit
    @user = User.find(params[:id])
    # don't care about prepopulation yet
    @reglines = Array.new(4) { String.new("test") }
  end

  <% @reglines.each_with_index do |line, index| %>
</p>
  <% fields_for "reglines[#{index}]", line do |f| %>
    First Name: <%= f.text_field :line  %>
  <% end %>
<% end %>

I don't know how to solve this, as I am - as you might have guessed - a bloody newbie. smile

Last edited by arnuschky (2007-01-30 14:45:37)

Re: Single string in models needs to be multiple lines in form

Rather than

   <% fields_for "reglines[#{index}]", line do |f| %>
     First Name: <%= f.text_field :line  %>
   <% end %>

which expects 'reglines[x]' to be a model with attributes you may want to just add these as text fields in your User form:
   ...start user form...
   <%  @reglines.each_with_index do |line, index| %>
     Reg #<%= index %>: <%= f.text_field "regline[#{index}]", :value => line  %>
   <%  end %>
   ....end user form...

And then you can add something like this in your controller:
def create
  complete_reglines = params[:regline].join
end

Re: Single string in models needs to be multiple lines in form

Or another way to solve this is with what I call a "fake attribute" (doing a search on this forum should show some related topics). This is great when you want to present data to the user in a way that is different than how it is stored in the database.

To do this you would create a getter and setter method for this fake attribute. Your case is a little more complicated than most because you have multiple lines/attributes, but we can handle this with an array.

class License < ActiveRecord::Base
  def reglines # returns an array of strings - in length of 25 characters each, always returns four elements
    regdata.scan(/.{25}/).values_at(0..3)
  end
 
  def reglines=(reglines) # accepts an array of strings. pads each line to 25 chars and puts them in regdata
    self.regdata = reglines.collect { |line| line.ljust(25) }.join
  end
end

When it comes to the form, you can have it group all the text fields into an array by adding "[]" to the name of the text field.

<%= text_field_tag "reglines[]" %>

Then in the controller you can fetch these with params[:reglines] which will return an array.

But, it gets better, by giving it this name:

<%= text_field_tag "license[reglines][]" %>

It will group all the fields and put them in a hash, so params[:license] will return a hash containing the attributes: {:reglines => [....]}

What does this mean? Take a look at this beautiful code. smile

# in controller
def new
  @license = License.new
end

def create
  @license = License.new(params[:license])
  if @license.save
    redirect_to :action => 'index'
  else
    render :action => 'new' # rerendering should fill in text fields again properly
  end
end


<% for line in @license.reglines %>
  <%= text_field_tag "license[reglines][]", line %>
<% end %>

Setting the ":reglines" attribute when creating the license model ends up calling the "reglines=" setter method we set up and everything should be handled properly.

You should be able to create the license model alongside the user model. I didn't show how to do that because I'm not sure how the two are related.

Railscasts - Free Ruby on Rails Screencasts

Re: Single string in models needs to be multiple lines in form

Wow, that was a fast reply! Thanks...

I solved the problem myself using callbacks and virtual attributes, but your solution is so much more elegant! Posts like this make me realize how much there is to learn, but there's always a way to do it clean and beautifully.

Additionally, your solution has the advantage of a working validates_uniqueness on regdata, which did not work with callbacks.

I just needed to add the striping of the spaces before using the data in the textfield and using the username as default value if the user didn't enter anything...

  def reglines 
    self.regdata.scan(/.{25}/).values_at(0..3).collect {|line| line.strip }
  end
 
  def reglines=(reglines)
    self.regdata = reglines.collect { |line| line.ljust(25) }.join
    if (self.regdata.strip.length == 0)   
      self.regdata = self.name.ljust(100)
    end
end

Again, thanks alot!

Last edited by arnuschky (2007-01-30 17:07:10)

Re: Single string in models needs to be multiple lines in form

Oh, wait a sec... This does work only on edits. I had to add the following:

  def reglines 
    if (self.regdata != nil)
      self.regdata.scan(/.{25}/).values_at(0..3).collect {|line| line.strip }
    else
      Array.new(4) { String.new }
    end
  end

Re: Single string in models needs to be multiple lines in form

Oh yeah, forgot to handle nil values. You can do this too.

  def reglines
    regdata.to_s.scan(/.{25}/).values_at(0..3).collect(&:strip)
  end

The line is getting kinda crazy though.

Railscasts - Free Ruby on Rails Screencasts

Re: Single string in models needs to be multiple lines in form

Yep, but just a little bit. wink

This collect(&:strip) is new to me. I tried to find something but I don't get this construct...

Just a last question: when I check for the uniqueness of the registration data, how can I mange to output an error like: "Registration data has already been taken." _and_ highlight all of the regline textfields? Is this possible?

Last edited by arnuschky (2007-01-30 17:33:06)

Re: Single string in models needs to be multiple lines in form

It's a shorthand for calling a method on each parameter passed in a block. See this post for details.

As for the error message, I'm assuming you know how to generate the message but are wondering how to highlight the text fields? Unfortunately I don't know of an easy way. You may need to do this manually by calling "@license.errors.invalid?(:regdata)" which should return true/false depending on if there's an error for that attribute. You can then set a CSS class attribute or something to highlight the text fields.

Railscasts - Free Ruby on Rails Screencasts

Re: Single string in models needs to be multiple lines in form

Ow, the short line still reports an error because of "nil.strip". But never mind, I'll stick with my "complicated" version - at least I understand that. smile

I highlighted the fields work like a charm, using your hint and the usual divs. Thanks!

I'm adding an error message link this:
[code:ruby]
    if self.errors.on(:regdata)
      self.errors.add_to_base('Registration data', 'has already been taken')
    end
[/code]

Is there a way to remove an error from the list? I guess not, at least I haven't found a suitable method in the Errors object. I guess I have to stick with the two error messages...

Last edited by arnuschky (2007-01-30 18:19:06)