Rails - how to query has_many from model - ruby-on-rails

I have a scheduleevent model. The model uses ice_cube to create recurrence rules. A scheduleevent has_many schedtimeranges. A schedtimnerange has a start_time, end_time and booleans for the days of the week Sunday - Saturday. Whatever days are checked are true.
What I need to do is get those days in an array to pass them into ice_cube. How would I join scheduleevent to schedtimeranges, and get Sunday - Saturday where the value is true in a rails model? Do I query the records in the controller, then loop them in the model, or if I need to loop in the model, query them from the model?
Trying to do something like this, but getting a no method schedtimeranges error:
def self.timeranges
Schedtimeranges.where(scheduleevent_id: self.id).columns.select{ |c| c.type == :boolean }.map(&:name)
end
weekdays = self.timeranges
Scheduleevent Model:
has_many :schedtimeranges, inverse_of: :scheduleevent
accepts_nested_attributes_for :schedtimeranges, allow_destroy: true
EDIT:
weekdays = schedtimeranges.where(scheduleevent_id: id).columns.select{ |c| c.type == :boolean }.map(&:name).to_a
This works, but I just need to only get them where the value is true. How do I add that to this? So if a scheduleevent has say 3 schedtimeranges, I want to get the days of week, and start_time/end_time where they are true.
Expected output would be 07:00 AM, 14:00 PM, Monday, Thursday, Friday. (If those are the 3 days that are true.
create_table "schedtimeranges", force: :cascade do |t|
t.bigint "scheduleevent_id", null: false
t.string "start_time"
t.string "end_time"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.boolean "monday"
t.boolean "tuesday"
t.boolean "wednesday"
t.boolean "thursday"
t.boolean "friday"
t.boolean "saturday"
t.boolean "sunday"
t.index ["scheduleevent_id"], name: "index_schedtimeranges_on_scheduleevent_id"
end
EDIT 2:
This is how I am trying to create the recurrence rules. How would I loop the to_ice_cube and pass the scheduleevent_id through?
schedule = IceCube::Schedule.new(start)
case orule
when 'daily'
schedule.add_recurrence_rule IceCube::Rule.daily(1).until(end_date.to_time)
when 'weekly'
self.map(&:to_ice_cube).where(scheduleevent_id: self.id).each do |days|
schedule.add_recurrence_rule IceCube::Rule.weekly(1).day(days.map{|s| s.to_sym}).until(end_date.to_time)
end
end
The idea being that if it is a daily repeat type I just have a start time and end time between all days froms tart date to end date. If it is a weekly recurrence I want to create the rules for the times on those particular days.
I am getting an undefined method map the scheduleevent record.

I think you made it sound way more complicated than it is. Expected output helped a lot. I've added a couple of methods to Schedtimerange to make it simpler:
# why not ScheduleTimeRange and ScheduleEvent
class Schedtimerange < ApplicationRecord
# sometimes you just have to type all the attributes that you need
def days
{
monday: monday,
tuesday: tuesday,
wednesday: wednesday,
thursday: thursday,
friday: friday,
saturday: saturday,
sunday: sunday
}
end
# # or like this
# DAYS = %w[monday tuesday wednesday thursday friday saturday sunday].freeze
# def days
# attributes.slice(*DAYS)
# end
# desired format goes here
def to_ice_cube
[start_time, end_time, *days.compact_blank.keys]
end
end
https://api.rubyonrails.org/classes/Hash.html#method-i-compact_blank-21
>> Schedtimerange.first.to_ice_cube
=> ["07:00 AM", "08:00 AM", :monday, :friday]
>> Schedtimerange.where(scheduleevent_id: 1).map(&:to_ice_cube)
=> [["07:00 AM", "08:00 AM", :monday, :friday], ["11:00 AM", "04:00 PM", :monday, :tuesday]]
Do the same for Scheduleevent if you want:
class Scheduleevent < ApplicationRecord
has_many :schedtimeranges
def to_ice_cube
[id, schedtimeranges.map(&:to_ice_cube)]
end
end
>> Scheduleevent.limit(2).map(&:to_ice_cube)
=> [[1, [["07:00 AM", "08:00 AM", :monday, :friday], ["11:00 AM", "04:00 PM", :monday, :tuesday]]],
[2, [[nil, nil, :monday, :tuesday], [nil, nil, :tuesday, :wednesday]]]]
If you're calling it from Scheduleevent, just use association to get relevant ranges:
schedtimeranges.map(&:to_ice_cube).each do |days|
schedule.add_recurrence_rule IceCube::Rule.weekly(1).day(days).until(end_date.to_time)
end

Related

Ruby on Rails changing date not saving to object

I'm trying to change a 'time' entry with the date entered in the 'date' entry. So if the time is "2000-01-01 10:00:00 UTC" and the date is "2021-10-10" I want the output to be "2021-10-10 10:00:00 UTC".
I almost have it working, however; when I assign the updated date back to the original object, it does not save the change. For instance, in the code below, event_time contains the proper time I want, however, assigning it to #event.time and then printing #event.time shows the change did not take place.
def create
#event = Event.new(event_params)
event_date = #event.date
event_time = #event.time.change(:year => event_date.year, :month => event_date.month, :day => event_date.day)
puts event_time # prints 2021-10-22 06:06:00 UTC
#event.time = event_time
puts #event.time # prints 2000-01-01 06:06:00 UTC
if #event.save
redirect_to(events_path)
else
render('new')
end
end
Any suggestions? I'm new to Ruby so I'm probably missing something obvious here
Here's my schema
create_table "events", force: :cascade do |t|
t.date "date"
t.string "description"
t.boolean "isMandatory"
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
t.string "name"
t.time "time"
t.string "location"
end
You can refer to the SO answer here
The problem is that there is no time-of-day class in Ruby or Rails. All the time classes are dates or timestamps (i.e. date plus time of day).
Inside the database it will be a time (without timezone) column and it will behave properly inside the database. However, once the time gets into Ruby, ActiveRecord will add a date component because there is no plain time-of-day class available, it just happens to use 2000-01-01 as the date.
Everything will be fine inside the database but you'll have to exercise a little bit of caution to ignore the date component when you're outside the database in Rails.
Use datetime column type to hold a date and time. Only use time in the migration if you don't need the date (only want to store time part).

Rails calculate Date duration from start_date and end_date

In my Rails 6 app I've got a model with fields:
create_table "events", force: :cascade do |t|
t.date "start_date", null: false
t.date "end_date"
t.integer "duration"
end
When duration is not provided I need to have mechanism which calculates the missing value if start_date and end_date are given. The solution is pretty simple I guess:
def duration_calculator(start_date, end_date)
end_date - start_date
end
2.7.1 :049 > start_date
=> Mon, 15 Feb 2021
2.7.1 :050 > end_date
=> Mon, 15 Feb 2021
2.7.1 :051 > end_date - start_date
=> (0/1)
But it won't worked - I was expecting to get 1 since the event lasted all day. How to achieve that behaviour?
To avoid the rational number, you could use a inclusive range and count the number of days via:
(start_date..end_date).count
Unfortunately, count actually traverses the range instead of just calculating the difference.
You could calculate the difference yourself using jd – the Julian day number:
end_date.jd - start_date.jd + 1
Or combine both of the above into:
(start_date.jd..end_date.jd).count
Here, count calculates the difference, because it is an integer range.
Note that the range-based solutions don't allow negative results because ranges are always ascending in Ruby.
In your case, start_date is equal to end_date as both are Mon, 15 Feb 2021, so the result in console is correct. The returned fraction can be cast into an integer format by (end_date - start_date).to_i, but of course this will still return 0.
You should use DateTime instead of Date if you want to be more exact on timing of events, or you could adjust your helper to the following to ensure the smallest duration is 1:
def duration_calculator(start_date, end_date)
if end_date == start_date
1
else
end_date - start_date
end
end
Note, you would have to consider here about an event with a start_date of today, and an end_date of tomorrow, as that would also return 1.
EDIT
Updating the method above so the method will return the number of days spanned:
def duration_calculator(start_date, end_date)
duration_in_seconds = end_date.at_end_of_day - start_date.at_beginning_of_day
duration_in_days = duration_in_seconds/86400 # seconds in one day
duration_in_days.to_i # round to nearest day
end

Migrating existing Time to DateTime in Rails while keeping data intact

Forgive me if I'm asking a silly question, but I've run into some trouble trying to update a time column to datetime.
Here's what the relevant part of my schema.rb looks like:
create_table "shop_hours", id: :serial, force: :cascade do |t|
t.time "from_hours"
t.time "to_hours"
t.string "day"
t.integer "repair_shop_id"
t.boolean "is_shop_open"
t.integer "chain_id"
t.integer "regions", default: [], array: true
t.index ["repair_shop_id"], name: "index_shop_hours_on_repair_shop_id"
end
Here's an example of random ShopHour object:
[67] pry(main)> ShopHour.find(439)
#<ShopHour:0x00007ff05462d3a0
id: 439,
from_hours: Sat, 01 Jan 2000 15:00:00 UTC +00:00,
to_hours: Sat, 01 Jan 2000 00:00:00 UTC +00:00,
day: "Friday",
repair_shop_id: 468,
is_shop_open: true,
chain_id: nil,
regions: []>
Ultimately, I want to migrate the attributes from_hours and to_hours on all of my ShopHour tables so that they're of type datetime.
I'd also like to update the date on each from_hours and to_hours to be current.
I tried this migration, but ran into an error:
class ChangeShopHoursToDateTime < ActiveRecord::Migration[6.0]
def change
change_column :shop_hours, :from_hours, 'timestamp USING CAST(from_hours AS timestamp)'
change_column :shop_hours, :to_hours, 'timestamp USING CAST(to_hours AS timestamp)'
end
end
Here's the error I'm encountering:
== 20201021083719 ChangeShopHoursToDateTime: migrating ========================
-- change_column(:shop_hours, :from_hours, "timestamp USING CAST(from_hours AS timestamp)")
rails aborted!
StandardError: An error has occurred, this and all later migrations canceled:
PG::CannotCoerce: ERROR: cannot cast type time without time zone to timestamp without time zone
LINE 1: ...s" ALTER COLUMN "from_hours" TYPE timestamp USING CAST(from_...
Please let me know if I can provide any more information. Thanks in advance!
You can't automatically actually cast a time column to a timestamp as a time has no date component. Postgres actually correctly prevents you from doing this as the result would be ambiguous - which date should it really cast 12:45 to:
0 BC?
the beginning of epoc time?
todays date?
Ruby doesn't actually have a class to represent a time without a date component. The major difference is that Time is simple wrapper written in C that wraps a UNIX timestamp and DateTime is better at historical times. The fact that Rails just casts a time database column to a Time starting at 2000-01-01 is really just a strange yet pragmatic solution to the problem instead of creating something like a TimeWithoutDate class.
If you want to migrate a database column from time to timestamp / timestampz you need to tell the database which date you expect the time to be at:
class AddDatetimeColumnsToHours < ActiveRecord::Migration[6.0]
def up
add_column :shop_hours, :opens_at, :datetime
add_column :shop_hours, :closes_at, :datetime
ShopHour.update_all(
[ "closes_at = (timestamp '2000-01-01') + to_hour, opens_at = (timestamp '2000-01-01') + from_hour" ]
)
end
def down
remove_column :shop_hours, :opens_at
remove_column :shop_hours, :closes_at
end
end
This adds two new columns and you should really consider just dropping the existing column and going with this naming scheme as methods that start with to_ are by convention casting methods in Ruby (for example to_s, to_a, to_h) - to_hour is thus a really bad name.

How do you convert all timestamps to Unix timestamps?

My iOS developer was asking me to provide all timestamps in a JSON structure to be Unix timestamps, as the number of seconds that have elapsed since "00:00:00 Coordinated Universal Time (UTC), Thursday, 1 January 1970", not counting leap seconds.
Seeing that all of my data was stored in strings on the server, I would like to create a method to convert all of the timestamps to Unix timestamps as an integer.
In other words, I need to convert:
{
"created_at" => "2015-01-18T00:58:58.903Z",
"updated_at" => "2015-01-18T00:58:58.903Z",
"user_id" => 1,
"status_id" => 1
}
to:
{
"created_at" => 1421542604,
"updated_at" => 1421542604,
"user_id" => 1,
"status_id" => 1
}
I wrote the following rspec, then realized I need to modify the default as_json method to perform this conversion:
expect(response_json).to eq(
{
'created_at' => user.created_at.as_json,
'updated_at' => user.updated_at.as_json,
'user_id' => user.user_id,
'status_id' => user.status_id
}
)
I then created /config/initializers/unix_timestamps.rb and it worked:
class ActiveSupport::TimeWithZone
def as_json
self.to_time.to_i
end
end

How to scope last week by Date object [duplicate]

This question already has an answer here:
Closed 11 years ago.
Possible Duplicate:
Scoping date attribute for this week?
I am trying to scope all of my Products for this week, so it should show all the products leading up to whichever day of the week.
class Product < ActiveRecord::Base
attr_accessible :purchase_date
def self.last_week # All prices of last week.
where(:purchase_date => 1.week.ago)
end
create_table :products do |t|
t.date :purchase_date
end
end
This code renders nothing in the view though so what do I need to correct?
ANSWER
For some reason I had to add advance(:days => -1) to in order to also retrieve Monday as well. You may not have to do this though.
def self.last_week
where(:purchase_date => 1.week.ago.beginning_of_week.advance(:days => -1)..1.week.ago.end_of_week).order("purchase_date desc")
end
UPDATED ANSWER
I had to do the advance(:days => -1) because of the Time zone I am in. I got rid of this by making sure I'm in my own Time zone. So now it can be normal as it should be:
def self.last_week
where(:purchase_date => 1.week.ago.beginning_of_week..1.week.ago.end_of_week)
end
And it should work correctly ONLY if you go by the default Rails Time zone or you config your own:
app/config/environment/development.rb
config.time_zone = "Eastern Time (US & Canada)"
Good luck.
This should do the trick:
scope :last_week, lambda { where("purchase_date >= :date", :date => 1.week.ago) }
scope :past_week, lambda { where("purchase_date >= :start_date AND purchase_date <= :end_date", {:start_date => 1.week.ago, :end_date => 1.day.ago }) }

Resources