Rails map two fields in form to one field in model - ruby-on-rails

I am using Rails 4.2rc3 and the simple_form gem.
I have an Event model which has a field reservations_open_at which indicates how many seconds before the event starts that reservations can start to be made (e.g. reservations might open up 2 days in advance, or maybe 10 hours in advance).
However, I want to use a form with a text input for a number (reservations_open_at_amount) and a select form for either 'Hours' or 'Days' (reservations_open_at_unit). I.e. the user can input '2 days' or '10 hours' via the two fields.
I can't quite figure out the right way to use virtual attributes to set the reservations_open_at field in seconds using a regular #event.update(event_params) call in my controller.
I don't need to remember whether the user chose Hours or Days when editing an existing record; if the reservations_open_at amount modulo (24*60*60) is zero (i.e. the number of seconds is an exact multiple of days) then I will display the form with reservations_open_at / (24*60*60) and Days, else reservations_open_at / (60*60) and Hours.

You are already more than half way there.
In the model, add two virtual attributes, time_amount, time_units. Add these 2 to the views.
Add a before_save callback to set the value in reservations_open_at.
Something like:
Edit: add getter for time unit
class Event < ActiveRecord::Base
attr_accessor :time_unit, time_amount
before_save :calculate_open_at
...
def time_unit
return #time_unit if #time_unit
if ( self.time_amount = reservations_open_at / 1.day.seconds ) > 0
#time_unit = 'days'
else
self.time_amount = reservations_open_at / 1.hour.seconds
#time_unit = 'hours'
end
end
def calculate_open_at
conversion_rate = time_unit == "days" ? 1.day.seconds : 1.hour.seconds
self.reservations_open_at = time_amount * conversion_rate
end
...
end

Related

How does one get the "next" record from database sorted by a specific attribute without loading all the records?

Here's the situation:
I have an Event model and I want to add prev / next buttons to a view to get the next event, but sorted by the event start datetime, not the ID/created_at.
So the events are created in the order that start, so I can compare IDs or get the next highest ID or anything like that. E.g. Event ID 2 starts before Event ID 3. So Event.next(3) should return Event ID 2.
At first I was passing the start datetime as a param and getting the next one, but this failed when there were 2 events with the same start. The param start datetime doesn't include microseconds, so what would happen is something like this:
order("start > ?",current_start).first
would keep returning the same event over and over because current_start wouldn't include microseconds, so the current event would technically be > than current_start by 0.000000124 seconds or something like that.
The way I got to work for everything was with a concern like this:
module PrevNext
extend ActiveSupport::Concern
module ClassMethods
def next(id)
find_by(id: chron_ids[current_index(id)+1])
end
def prev(id)
find_by(id: chron_ids[current_index(id)-1])
end
def chron_ids
#chron_ids ||= order("#{order_by_attr} ASC").ids
end
def current_index(id)
chron_ids.find_index(id)
end
def order_by_attr
#order_by_attr ||= 'created_at'
end
end
end
Model:
class Event < ActiveRecord::Base
...
include PrevNext
def self.order_by_attr
#order_by_attr ||= "start_datetime"
end
...
end
I know pulling all the IDs into an array is bad and dumb* but i don't know how to
Get a list of the records in the order I want
Jump to a specific record in that list (current event)
and then get the next record
...all in one ActiveRecord query. (Using Rails 4 w/ PostgreSQL)
*This table will likely never have more than 10k records, so it's not catastrophically bad and dumb.
The best I could manage was to pull out only the IDs in order and then memoize them.
Ideally, i'd like to do this by just passing the Event ID, rather than a start date params, since it's passed via GET param, so the less URL encoding and decoding the better.
There has to be a better way to do this. I posted it on Reddit as well, but the only suggested response didn't actually work.
Reddit Link
Any help or insight is appreciated. Thanks!
You can get the next n records by using the SQL OFFSET keyword:
china = Country.order(:population).first
india = City.order(:population).offset(1).take
# SELECT * FROM countries ORDER BY population LIMIT 1 OFFSET 1
Which is how pagination for example often is done:
#countries = Country.order(:population).limit(50)
#countries = scope.offset( params[:page].to_i * 50 ) if params[:page]
Another way to do this is by using would be query cursors. However ActiveRecord does not support this and it building a generally reusable solution would be quite a task and may not be very useful in the end.

Rails input comma separated value from text field and store in different columns

I have a model named RaceTimings which records the timing of each student in a race.
I want to take the input of the form in format minute:seconds:microseconds from a single text field and store the values in 3 different columns of the same model.
I have already been through other links but could not find any solution.
Can anyone suggest how this can be done?
Just use the def something=(val) function which you call all the time when you are using a = to set some variable.
class RaceTiming
# unless you dont have those fields in your database
attr_accessor :minutes, :seconds, :microseconds
def time
# returns "12:14:24" as a string
[minutes, seconds, microseconds].join(":")
end
def time=(val)
#split the val into an array ["11", "04", "123"] if it was 11:04:123
val = val.split(":")
self.minutes = val[0]
self.seconds = val[1]
self.microseconds = val[2]
end
end
you call it by
record = RaceTiming.new
record.time = "12:44:953"
# do what you need to do
Assuming that your text field is giving the minutes, seconds and microseconds in "minute:seconds:microseconds" format, you can do something like this:
a = "minute:seconds:microseconds".split(':')
minutes, seconds, microseconds = a[0], a[1], a[2]
RaceTiming.update_attributes(:minutes => minutes, :seconds => seconds, :microseconds => microseconds)

Methods depending on each other

I'm building a complex billing system with Ruby on Rails. I have an Invoice model that has many items and payments. The invoice is considered paid when the paid_cents are greater than or equal to the total_cents.
Everything worked fine until I added support for late fees. Obviously, these fees should only be calculated when the invoice hasn't been paid. For instance, if the invoice was due on 2014-01-01, it has been paid on 2014-01-05 and today is 2014-01-24, the fees should be calculated for 4 days only.
In order to accomplish that, I added a check to the late_days method, making it return 0 if paid_cents was greater than or equal to total_cents AND the last payment was delivered before the invoice's due date.
Here's the issue: the total_cents are calculated summing the items' amounts and the late fees. In turn, the late_fee_cents method calls late_days which relies on total_cents to check whether the invoice has been paid.
This results in an infinite loop where the methods will call each other till the end of time (or someone pulls the plug).
Here's some code from my model:
def daily_late_fee_percentage
return 0 unless late_fee_percentage && late_fee_interval
late_fee_percentage.to_f / late_fee_interval.to_f
end
def daily_late_fee_cents
incomplete_total_cents.to_f / 100.0 * daily_late_fee_percentage
end
def late_days
return 0 unless due_at
days = Date.today - due_at
days = 0 if days < 0
days.to_i
end
def late_fee_cents
daily_late_fee_cents * late_days
end
def total_cents
incomplete_total_cents + late_fee_cents
end
I feel like I'm completely missing something obvious.
Any help would be much appreciated!
How should late_fee_cents be calculated? Is it only a fraction of the invoice's initial "total cents" excluding "late fees" multiplied by the days the invoice is overdue, or is it a fraction of the initial total cents plus late fees multiplied by the days the invoice is overdue?
If the former is true, then this might work:
We can define total_cents like this:
def total_cents(include_late_fees = true)
include_late_fees ? incomplete_total_cents + late_fee_cents : incomplete_total_cents
end
And late_days like this:
def late_days
return 0 unless due_at
return 0 if paid_cents >= total_cents(false) && (payments.last.try(:delivered_at) || due_at) <= due_at
days = Date.today - due_at
days = 0 if days < 0
days.to_i
end
Note that in late_days we are using total_cents(false).
This should eliminate the infitie loop.
If the latter is true, then first write down the exact mathematical formula for calculating the total invoice fee, including late fees and make sure
it doesn't have infinite recursion. The formula might contain sigma (Σ) or (Π) expressions.
Then it should be easy to convert it to code.
EDIT:
I think what you are trying to achieve is basically charging an interest on the debt (overdue fee). This is similar to computing compound interest.

Using a duration field in a Rails model

I'm looking for the best way to use a duration field in a Rails model. I would like the format to be HH:MM:SS (ex: 01:30:23). The database in use is sqlite locally and Postgres in production.
I would also like to work with this field so I can take a look at all of the objects in the field and come up with the total time of all objects in that model and end up with something like:
30 records totaling 45 hours, 25 minutes, and 34 seconds.
So what would work best for?
Field type for the migration
Form field for the CRUD forms (hour, minute, second drop downs?)
Least expensive method to generate the total duration of all records in the model
Store as integers in your database (number of seconds, probably).
Your entry form will depend on the exact use case. Dropdowns are painful; better to use small text fields for duration in hours + minutes + seconds.
Simply run a SUM query over the duration column to produce a grand total. If you use integers, this is easy and fast.
Additionally:
Use a helper to display the duration in your views. You can easily convert a duration as integer of seconds to ActiveSupport::Duration by using 123.seconds (replace 123 with the integer from the database). Use inspect on the resulting Duration for nice formatting. (It is not perfect. You may want to write something yourself.)
In your model, you'll probably want attribute readers and writers that return/take ActiveSupport::Duration objects, rather than integers. Simply define duration=(new_duration) and duration, which internally call read_attribute / write_attribute with integer arguments.
In Rails 5, you can use ActiveRecord::Attributes to store ActiveSupport::Durations as ISO8601 strings. The advantage of using ActiveSupport::Duration over integers is that you can use them for date/time calculations right out of the box. You can do things like Time.now + 1.month and it's always correct.
Here's how:
Add config/initializers/duration_type.rb
class DurationType < ActiveRecord::Type::String
def cast(value)
return value if value.blank? || value.is_a?(ActiveSupport::Duration)
ActiveSupport::Duration.parse(value)
end
def serialize(duration)
duration ? duration.iso8601 : nil
end
end
ActiveRecord::Type.register(:duration, DurationType)
Migration
create_table :somethings do |t|
t.string :duration
end
Model
class Something < ApplicationRecord
attribute :duration, :duration
end
Usage
something = Something.new
something.duration = 1.year # 1 year
something.duration = nil
something.duration = "P2M3D" # 2 months, 3 days (ISO8601 string)
Time.now + something.duration # calculation is always correct
I tried using ActiveSupport::Duration but had trouble getting the output to be clear.
You may like ruby-duration, an immutable type that represents some amount of time with accuracy in seconds. It has lots of tests and a Mongoid model field type.
I wanted to also easily parse human duration strings so I went with Chronic Duration. Here's an example of adding it to a model that has a time_spent in seconds field.
class Completion < ActiveRecord::Base
belongs_to :task
belongs_to :user
def time_spent_text
ChronicDuration.output time_spent
end
def time_spent_text= text
self.time_spent = ChronicDuration.parse text
logger.debug "time_spent: '#{self.time_spent_text}' for text '#{text}'"
end
end
I've wrote a some stub to support and use PostgreSQL's interval type as ActiveRecord::Duration.
See this gist (you can use it as initializer in Rails 4.1): https://gist.github.com/Envek/7077bfc36b17233f60ad
Also I've opened pull requests to the Rails there:
https://github.com/rails/rails/pull/16919

Rails: Validate that all fields in a date_select were selected but still set :prompt to true

When using Rails date_select with :prompt => true I see some very strange behavior when submitting the form without all fields selected. Eg.
Submitting the form with January selected but the day and year fields left at the default prompt results in January 1st 0001 getting passed to the model validation. If validation fails and the form is rendered again January is still selected (correctly) but the day is set to 1 (incorrectly). If the form is submitted with just the year selected, both month and day get set to 1.
This is very strange behavior - can anyone give me a workaround?
The problem has to do with multiparameter assignment. Basically you want to store three values into one attribute (ie. written_at). The date_select sends this as { 'written_at(1)' => '2009', 'written_at(2)' => '5', 'written_at(3)' => '27' } to the controller. Active record packs these three values into a string and initializes a new Date object with it.
The problem starts with the fact that Date raises an exception when you try to instantiate it with an invalid date, Date.new(2009, 0, 1) for instance. Rails catches that error and instantiates a Time object instead. The Time class with timezone support in Rails has all kinds of magic to make it not raise with invalid data. This makes your day turn to 1.
During this process active record looses the original value hash because it packed the written_at stuff into an array and tried to create a Date or Time object out of it. This is why the form can't access it anymore using the written_at_before_time_cast method.
The workaround would be to add six methods to your model: written_at_year, written_at_year=, and written_at_year_before_type_cast (for year, month and day). A before_validation filter can reconstruct the date and write it to written_at.
class Event < ActiveRecord::Base
before_validation :reconstruct_written_at
def written_at_year=(year)
#written_at_year_before_type_cast = year
end
def written_at_year
written_at_year_before_type_cast || written_at.year
end
def written_at_year_before_type_cast
#written_at_year_before_type_cast
end
private
def reconstruct_written_at
written_at = Date.new(written_at_year, written_at_month, written_at_day)
rescue ArgumentError
written_at = nil
end
end

Resources