Manually extract date from Rails date model object helpers - ruby-on-rails

I'm working with a Rails (v5.1) form that uses model object date helpers which submits dates selected via three dropdowns (d/m/y) as parameters like
{..."next_inspection_date(3i)"=>"14", "next_inspection_date(2i)"=>"8", "next_inspection_date(1i)"=>"2019"...}
By Rails magic, MyModel.new(params) knows what to do with this, but I have some extra logic I'd like to apply when a form is submitted. I could write something like
TimeWithZone.parse("#{params[:next_inspection_date(1i)]}-#{params[:next_inspection_date(2i)]}-#{params[:next_inspection_date(3i)]}")
to extract a date but this will get repetitive quickly with several dates. To DRY it up manually will likely produce something pretty terse to read and introduce a lot of logic that will really distract from what I'm trying to do. I was wondering if Rails has a built-in helper, so that I could write something like
MyModel.next_inspection_date = useful_date_helper(params, :next_inspection_date)
The Rails guides merely state that methods like update or new understand what to do with these params.
Thanks in advance for any suggestions.

These attributes are known as multi-parameter attributes and the the functionality is unfortunately buried deep inside of ActiveRecord::AttributeAssignment. But you can just use ActiveRecord's implementation without saving the record:
next_inspection_date = MyModel.new(params.permit(:next_inspection_date)).next_inspection_date
You could also replicate it which is really more interesting if you are building something like query objects or virtual models:
module MultiparamExtractor
# Extracts 'i' from foo(3i)
REGEXP = /\w*\(\d*(?<type>[a-z])\)/.freeze
# Extracts an array of cast values from a hash containing one multi-parameter attribute
# #param hash [Hash|ActionController::Parameters]
# #return [Array]
def self.call(multi_params)
# coerce input into a hash
if multi_params.respond_to?(:permit)
multi_params.dup.permit!.to_h # .dup prevents mutating the arguments
else
multi_params
end.sort # sorts by key <=> other key and returns an array
.map do |(key,value)|
typecast(value, REGEXP.match(key)[:type])
end
end
private
# #todo handle casting more types
def self.typecast(value, type)
case type
when 'i'
value.to_i
else
value
end
end
end
# Just for the sake of the example
params = ActionController::Parameters.new("next_inspection_date(3i)"=>"14", "next_inspection_date(2i)"=>"8", "next_inspection_date(1i)"=>"2019", "foo" => "bar")
date = Date.new(*MultiparamExtractor.call(params.permit(:next_inspection_date))).in_time_zone
# Wed, 14 Aug 2019 00:00:00 UTC +00:00

Related

Rails find_or_create_by add cast to hash value json type attribute?

I have a model with an :extra_fields column that is :jsonb datatype, I want to add in the attr hashes to the column, something like this below but I am unsure of the syntax to cast the hash values' datatypes here, and if not here what is the best practice for casting hash value data ?
instance = Model.find_or_create_by(ref_id: hash[:ref_id]) do |a|
a.extra_fields = {
'attr1' : hash[:attr1], <-- //possible to cast type here ie ::type ?
'attr2' : hash[:attr2] <--
}
instance.save!
end
Bonus: how would I cast the hash values as type :decimal, :string, :boolean, :date for example?
All incoming parameters in Rails/Rack are strings. Well except except array/hash parameters which still have strings as values. Rails does the actual casting when you pass parameters to models.
You can cast strings to any other type in Ruby with the .to_x methods:
irb(main):006:0> "1.23".to_f
=> 1.23
irb(main):007:0> "1.23".to_d
=> #<BigDecimal:7ff7dea40b68,'0.123E1',18(18)>
irb(main):008:0> 1.23.to_s
=> "1.23"
irb(main):008:0> 1.23.to_i
=> 1
Boolean casting is a Rails feature. You can do it by:
# Rails 5
ActiveModel::Type::Boolean.new.cast(value)
ActiveModel::Type::Boolean.new.cast("true") # true
ActiveModel::Type::Boolean.new.cast("t") # true
ActiveModel::Type::Boolean.new.cast("false") # false
ActiveModel::Type::Boolean.new.cast("f") # false
# This is somewhat surprising
ActiveModel::Type::Boolean.new.cast("any arbitrary string") # true
# Rails 4.2
ActiveRecord::Type::Boolean.new.type_cast_from_database(value)
# Rails 4.1 and below
ActiveRecord::ConnectionAdapters::Column.value_to_boolean(value)
Note that this is very different then the Ruby boolean coercion done by ! and !!.
irb(main):008:0> !!"false"
(irb):8: warning: string literal in condition
=> true
In Ruby everything except nil and false are true.
Dates are somewhat more complex. The default Rails date inputs use multi-parameters to send each part of the date (year, month, day) and a special setter that constructs a date from these inputs.
Processing by PeopleController#create as HTML
Parameters: { "person"=>{"birthday(1i)"=>"2019", "birthday(2i)"=>"2", "birthday(3i)"=>"16"}, ...}
You can construct a date from these parameters by:
date_params = params.fetch(:person).permit("birthday")
Date.new(*date_params.values.map(&:to_i))
what is the best practice for casting hash value data ?
There is no best practice here. What you instead should be pondering is the use of a JSON column. Since you seem to be want to apply some sort of schema to the data it might be a good idea to actually create a separate table and model. You are after all using a relational database.
JSON columns are great for solving some complex issues like key/value tables or storing raw JSON data but they should not be your first choice when modelling your data.
See PostgreSQL anti-patterns: Unnecessary json/hstore dynamic columns for a good write up on the topic.

What is the most elegant Ruby expression for comparing and selecting values from a 2D Array?

I have some code that is chugging through a set of Rails Active Record models, and setting an attribute based on a related value from a 2D Array.
I am essentially setting a US State abbreviation code in a table of US States which was previously only storing the full names. A library of state names is being used to derive the abbreviations, and it contains a 2D Array with each sub-array having a full name, and an abbreviation (i.e., [['New York', 'NY']['Pennsylvania', 'PA'][etc]]). I compare the state name from each record in the database to each full text name in this Array, then grab the corresponding sibling Array cell when there is a match.
This code works fine, and produces the correct results, but its frumpy looking and not easily understood without reading many lines:
# For the following code, StatesWithNames is an Active Record model, which is
# having a new column :code added to its table.
# Sates::USA represents a 2D Array as: [['StateName', 'NY']], and is used to
# populate the codes for StatesWithNames.
# A comparison is made between StatesWithNames.name and the text name found in
# States::USA, and if there is a match, the abbreviation from States::USA is
# used
if StatesWithNames.any?
StatesWithNames.all.each do |named_state|
if named_state.code.blank?
States::USA.each do |s|
if s[0] == named_state.name
named_state.update_column(:code, s[1])
break
end
end
end
end
end
What is the most Ruby style way of expressing assignments with logic like this? I experimented with a few different procs / blocks, but arrived at even cludgier expressions, or incorrect results. Is there a more simple way to express this in fewer lines and/or if-end conditionals?
Yea, there is a few ifs and checks, that are not needed.
Since it is Rails even though it does not state so in question's tags, you might want to use find_each, which is one of the most efficient way to iterate over a AR collection:
StatesWithNames.find_each do |named_state|
next unless named_state.code.blank?
States::USA.each do |s|
named_state.update_column(:code, s[1]) if s[0] == named_state.name
end
end
Also be aware, that update_column bypasses any validations, and if you wish to keep your objects valid, stick to update!.
And last thing - wrap it all in transaction, so if anything goes wrong all the way - it would rollback any changes.
StatesWithNames.transaction do
StatesWithNames.find_each do |named_state|
next unless named_state.code.blank?
States::USA.each do |s|
named_state.update!(:code, s[1]) if s[0] == named_state.name
end
end
end
You might use a different data structure for this.
With your existing 2D array, you can call to_h on it to get a Hash where
a = [['California', 'CA'], ['Oregon', 'OR']].to_h
=> { 'California' => 'CA', 'Oregon' => 'OR' }
Then in your code you can do
state_hash = States::USA.to_h
if StatesWithNames.any?
StatesWithNames.all.each do |named_state|
if named_state.code.blank?
abbreviation = state_hash[named_state.name]
if !abbreviation.nil?
named_state.update_column(:code, abbreviation)
end
end
end
end
the first thing you want to do is convert the lookup from an array of arrays to a hash.
state_hash = States::USA.to_h
if StatesWithNames.any?
StatesWithNames.all.select{|state| state.code.blank?}.each do |named_state|
named_state.update_column(:code, state_hash[named_state.name]) if state_hash[named_state.name]
end
end

Fill array with nils for when nothing returned from query in ruby (RoR)

I have a model called foo with a date field.
On my index view, I am showing a typical "weekly view" for a specified week. To put the data in my view, I loop through each day of the specified week and query the data one day at time. I do this so that I can make sure to put a NIL on the correct day.
foos_controller.rb
for day in 0..6
foo = Foo.this_date(#date+day.days).first
#foos[day] = foo
end
index.html.haml
- for day in 0..6
%li
- if #foos[day].nil?
Create a new foo?
- else
Display a foo information here
Obviously, there's a lot of things wrong here.
I should find someone smart member to tell me how to write a good query so that I only have to do it once.
I should not have any if/else in my view
My goal here is to either show the content if the it is there for a particular day or show a "create new" link if not.
thanks for the help in advance!!
First, I have no idea what this_date actually does, but I'll assume it's retrieving a record with a specific date from your datastore. Instead of doing 7 queries, you can condense this into one using a date range:
Foo.where(date: (#date..(#date + 6.days)))
You can tack on a .group_by(&:date) to return something similar to the hash you are manually constructing, but using the actual dates as keys instead of the date offset.
To iterate over the dates in the view, I would recommend using Hash#fetch, which allows you to define a default return when a key is not present, e.g:
hash = { :a => 1, :b => 2 }
hash.fetch(:a){ Object.new } #=> 1
hash.fetch(:c){ Object.new } # #<Object:...>
The question now is what object to substitute for nil. If you want to avoid using conditionals here, I'd recommend going with the NullObject pattern (you could involve presenters as well but that might be a bit overkill for your situation). The idea here is that you would create a new class to substitute for a missing foo, and then simply define a method called to_partial_path on it that will tell Rails how to render it:
class NullFoo
def to_partial_path
"null_foos/null_foo"
end
end
You'll need to create partials at both app/views/foos/_foo.html.erb and app/views/null_foos/_null_foo.html.erb that define what to render in each case. Then, in your view, you can simply iterate thusly:
<% (#date..(#date + 6.days)).each do |date| %>
<%= render #foos.fetch(date){ NullDate.new } %>
<% end %>
Is this appropriate for your situation? Maybe it's also a bit overkill, but in general, I think it's a good idea to get in the habit of avoid nil checks whenever possible. Another benefit of the NullObject is that you can hang all sorts of behavior on it that handle these situations all throughout your app.

Rails Save Date Range to Database

I have a form field that posts a date range in the format "05/14/2013 - 05/22/2013". My model has two separate date fields, begin and end for the respective beginning and end dates from the form field. What is the best MVC way to approach getting the date range into the correct fields?
I've been trying to manually deconstruct the date range in the controller's create method, but it looks like the updated params aren't properly seen in the model before the record is created.
EDIT:
The date range is coming in that format because I'm using Keith Wood's datepick, and it outputs the dates in a single input field.
What I've been trying to do currently is this (contract is the name of my model, and dates is the input date range:
beginDate = params[:dates].split("-")[0].strip()
endDate = params[:dates].split("-")[1].strip()
params.delete :dates
params[:contract][:begin] = Date.strptime(beginDate, '%m/%d/%Y')
params[:contract][:end] = Date.strptime(endDate, '%m/%d/%Y')
#contract = Contract.new(params[:contract])
... but these changes to params don't show up by the time the record is created and validated.
Define a setter on your model which takes the field, splits it, and puts each part into the appropriate field.
def date_range=(val)
begin_str, end_str = val.split(' - ')
self.begin_at = Date.parse(begin_str)
self.end_at = Date.parse(end_str)
end
This will work when called directly, or from a mass assignment method such as update_attributes or create. Make sure you add date_range (or the relevant param name) to attr_accessible if you already have this defined in your model.
I've changed the field names in my example, asbegin and end should be avoided as field names, since one is a method and the other is part of the ruby syntax.
You can use virtual attributes to make the conversion from the date range text into individual dates at the model level. Add a setter and getter as below in the model,
def date_range_text
return "#{start_date.to_s} - #{end_date.to_s}"
end
def date_range_text= val
start_date_text,end_date_text = val.split[" - "]
start_date = Time.zone.parse(start_date_text) unless start_date_text.nil?
end_date = Time.zone.parse(end_date_text) unless end_date_text.nil?
end
And use data_range_text in your forms. For me information, check out the railscast below.
http://railscasts.com/episodes/16-virtual-attributes-revised
This is the best way to handle the difference between database structure and the user input forms.

Formatting dates coming from params

In Grails, if I define a locale, and put a date on specific format on i18n file, like (dd/mm/AAAA), if call one request like:
http://myapp/myaction?object.date=10/12/2013
When I get print: params.date, it comes to me a date object.
How can I do the same on rails?
Normally the Rails handles this for you. For instance, the form helper datetime_select works in conjunction with some activerecord magic
to ensure ensure time/date types survive the round-trip. There are various alternatives to the standard date-pickers.
If this doesn't work for you e.g. rails isn't generating the forms, there are (at least) a couple of options.
One option, slightly evi, is to monkey-patch HashWithIndifferentAccess (used by request params) to do type conversions based on the key name. It could look something like:
module AddTypedKeys
def [](key)
key?(key) ? super : find_candidate(key.to_s)
end
private
# look for key with a type extension
def find_candidate(key)
keys.each do |k|
name, type = k.split('.', 2)
return typify_param(self[k], type) if name == key
end
nil
end
def typify_param(value, type)
case type
when 'date'
value.to_date rescue nil
else
value
end
end
end
HashWithIndifferentAccess.send(:include, AddTypedKeys)
This will extend params[] in the way you describe. To use it within rais, you can drop it into an initialiser, eg confg/initializers/typed_params.rb
To see it working, you can test with
params = HashWithIndifferentAccess.new({'a' => 'hello', 'b.date' => '10/1/2013', 'c.date' => 'bob'})
puts params['b.date'] # returns string
puts params['b'] # returns timestamp
puts params['a'] # returns string
puts params['c'] # nil (invalid date parsed)
However... I'm not sure it's worth the effort, and it will likely not work with Rails 4 / StrongParameters.
A better solution would be using virtual attributes in your models. See this SO post for a really good example using chronic.

Resources