I'm currently having trouble finding a nice way to code the following situation:
There is a Model called TcpService
, which has two attributes, port_from
and port_to
, both Integers. It also has a virtual attribute called portrange
, which is a String. portrange
is the String representation of the attributes port_from
and port_to
, so portrange = "80 90"
should yield port_from = 80
, port_to = 90
. What I'm trying to do now is using the same Formtastic
form for creating
AND updating
a TcpService
-object. The form looks pretty standard (HAML code):
= semantic_form_for @tcp_service do |f|
= f.inputs do
= f.input :portrange, as: :string, label: "Portrange"
-# calls @tcp_service.portrange to determine the shown value
= f.actions do
= f.action :submit, label: "Save"
The thing is, I don't know of a non-messy way to make the values I want appear in the form. On new
I want the field to be empty, if create
failed I want it to show the faulty user input along with an error, else populate port_from
and port_to
using portrange
. On edit
I want the String representation of port_from
and port_to
to appear, if update
failed I want it to show the faulty user input along with an error, else populate port_from
and port_to
using portrange
.
The Model looks like this, which seems quite messy to me. Is there a better way of making it achieve what I need?
class TcpService < ActiveRecord::Base
# port_from, port_to: integer
attr_accessor :portrange
validate :portrange_to_ports # populates `port_from` and `port_to`
# using `portrange` AND adds errors
# raises exception if conversion fails
def self.string_to_ports(string)
... # do stuff
return port_from, port_to
end
# returns string representation of ports without touching self
def ports_to_string
... # do stuff
return string_representation
end
# is called every time portrange is set, namely during 'create' and 'update'
def portrange=(val)
return if val.nil?
@portrange = val
begin
self.port_from, self.port_to = TcpService.string_to_ports(val)
# catches conversion errors and makes errors of them
rescue StandardError => e
self.errors.add(:portrange, e.to_s())
end
end
# is called every time the form is rendered
def portrange
# if record is freshly loaded from DB, this is true
if self.port_from && self.port_to && @portrange.nil?
self.ports_to_string()
else
@portrange
end
end
private
# calls 'portrange=(val)' in order to add errors during validation
def portrange_to_ports
self.portrange = self.portrange
end
end
Thanks for reading
In your model
def portrange
return "" if self.port_from.nil? || self.port_to.nil?
"#{self.port_from} #{self.port_to}"
end
def portrange=(str)
return false unless str.match /^[0-9]{1,5}\ [0-9]{1,5}/
self.port_from = str.split(" ").first
self.port_to = str.split(" ").last
self.portrange
end
Using this you should be able tu use the portrange
setter and getter in your form.