Chris
Chris

Reputation: 176

Change user input from view before saving in model

I have a model that accepts a decimal (SQLite3 development database). I want the user to input a string however, for example "1:15:15" (which can be human-read as 1 hour, 15 minutes and 15 seconds, or, 75 minutes and 15 seconds). In my model I have a before_save that calls a custom private method to convert the string to a number (note that using private or not has the same result):

class LogEntry < ApplicationRecord
  belongs_to :user
  default_scope -> { order(date: :desc) }
  before_save :convert_runtime

  def convert_runtime
    a = self.runtime.split(':')
    #debugger
    self[:runtime] = a[0].to_f * 3600 + a[1].to_f * 60 + a[2].to_f 
  end
end

The split gave me an error so when I inserted the debugger code and changed the line with the split to only a = self.runtime (to see what this value actually is) the debugger yields a value of 0.1e1 for the "a" variable (a is for array). So what Rails seems to do is convert the first value in the string coming from the user (before the ":") to a decimal, ignoring the remainder of the string, before it's saved. Is there a way to manipulate the entire string as it's passed from the view to the model, without Rails first converting it to decimal form? My next option is to change the database to save the string. I feel like this is less ideal because if I want to add up years of entries, each string needs to be converted to a number first, so I'd rather store the time value as seconds, and convert upon input.

I've been stuck on this one for awhile.

Upvotes: 0

Views: 626

Answers (3)

3limin4t0r
3limin4t0r

Reputation: 21110

I would create a custom setter that does not override an existing method. Some would call this a computed attribute.

class LogEntry < ApplicationRecord
  belongs_to :user
  default_scope -> { order(date: :desc) }

  def runtime_string
    return unless runtime
    hours, minutes   = runtime.divmod(3600)
    minutes, seconds = minutes.divmod(60)
    [hours, minutes, seconds].drop_while(&:zero?).join(':').presence || '0'
  end

  def runtime_string=(value)
    seconds, minutes, hours = value.split(':').map(&:to_i).reverse
    self.runtime = (seconds || 0) + (minutes || 0) * 60 + (hours || 0) * 3600
  end
end

Then use this new getter/setter in your form.

The reason I reverse the split value is to handle both the strings "10", "1:15" and "1:2:34". However if you know the input is always H:M:S then there is no need to reverse the split output.

Upvotes: 1

Chris
Chris

Reputation: 176

It turns out this works, and doesn't require the before_save line:

def runtime=(runtime)
   a = runtime.split(":")
   self[:runtime] = a[0].to_f * 3600 + a[1].to_f * 60 + a[2].to_f 
end

However, it's not private and I'm not sure if this is a security risk.

Upvotes: 0

Sergio Tulentsev
Sergio Tulentsev

Reputation: 230286

You could use a transient accessor for your string representation, so that activerecord's typing helpers don't cast your value. Something like this:

class LogEntry < ApplicationRecord
  belongs_to :user
  default_scope -> { order(date: :desc) }

  attr_accessor :runtime_string

  before_save :convert_runtime

  def convert_runtime
    a = runtime_string.split(':')
    #debugger
    self[:runtime] = a[0].to_f * 3600 + a[1].to_f * 60 + a[2].to_f 
  end
end

Naturally, your form must use runtime_string now

<%= f.text_field :runtime_string %>

Upvotes: 2

Related Questions