Access JSON field of join table with Active Record - ruby-on-rails

One shop has_many products and a product belongs_to a shop.
The shop has opening_hours and closing_hours columns. Each column stores a hash which looks like the following:
opening_hours = {'Monday' => '12:00', 'Tuesday' => '13:00'}
and so on
I am working with current date and time, and I would like to retrieve all products which stores have opening_hours smaller than current time which is smaller than closing_hours.
Basically all the products that are available when the shop is open.
I did something like that in my controller:
day_today = Date.today.strftime('%A')
time_now = Time.new.strftime('%H:%M')
#products = Product.joins(shop: [{opening_hours[day_today] < time_now,
{closing_hours[day_today] > time_now }])
Edit
This is the code that stores the opening times in the DB
hours_table = shop.search('table')
opening_hours = {}
closing_hours = {}
hours_table.first.search('tr').each do |tr|
cells = tr.search('td')
day = cells.first.text.strip
hours = cells.last.text.strip.tr('-', '').split
opening_hours[day] = hours[0].to_time.strftime('%H:%M')
closing_hours[day] = hours[1].to_time.strftime('%H:%M')
end
shop = Shop.new(
name: shop_name,
opening_hours: opening_hours,
closing_hours: closing_hours
)
shop.save
new_product = Product.new(
name: foo,
description: foo,
price: foo,
company: 'foo',
)
new_product.shop = shop
new_product.save
This is the migration to add opening hours to Shop. The same is done for closing hours.
class AddOpeningHoursToShop < ActiveRecord::Migration[5.1]
def change
add_column :shops, :opening_hours, :json, default: {}
end
end
This is the migration to add the shop ID to products
class AddShopToProducts < ActiveRecord::Migration[5.1]
def change
add_reference :products, :shop, foreign_key: true
end
end
Any guess?

You can use the ->> operator to access a specific JSON field as text:
SELECT opening_hours->>'Monday' AS monday_opening_hour FROM shops;
Same for closing_hours, it just changes the name of the column and field.
So the AR representation in your case would be like:
Product
.joins(:shop)
.where(
"shops.opening_hours->>:day <= :now AND shops.closing_hours->>:day >= :now",
day: day_today,
now: time_now
)
You can easily bind the key name and the time you're going to ask for, here as day and now.
Notice, it's not needed to explicitly prepend the table name, but I'm doing so just for the sake of clarity.
This can also be done with the BETWEEN operator:
Product
.joins(:shop)
.where(
":now BETWEEN shops.opening_hours->>:day AND shops.closing_hours->>:day",
day: day_today,
now: time_now
)
You can use the one you think is more clear.
In any case, you can get rid of storing the time_now variable, by using the NOW function of PostgreSQL. It'll handle the time format difference if you cast it to text (NOW()::text):
Product
.joins(:shop)
.where(
"NOW()::text BETWEEN shops.opening_hours->>:day AND shops.closing_hours->>:day",
day: day_today
)
Similarly, the day can also be obtained from NOW. You can use to_char(NOW(), 'Day') for that
SELECT to_char(date, 'Day') AS day
FROM generate_series (NOW() - '6 days'::interval, NOW(), '1 day') date;
day
-----------
Thursday
Friday
Saturday
Sunday
Monday
Tuesday
Wednesday
So:
Product
.joins(:shop)
.where("NOW()::text BETWEEN shops.opening_hours->>(to_char(NOW(), 'Day'))
AND shops.closing_hours->>(to_char(NOW(), 'Day'))")

Related

Rails order by calculation in associated table

I’m working with the below Rails Models.
Artist.rb
has_many: updates
Update.rb
belongs_to: artist
Updates has a popularity column (int 0-100)
I need to order artists by difference in popularity within the last 30 days. (last row - first row of updates in range)
I’ve made this work in controller by iterating over list of artists, calculate the difference in popularity, and save that value together with the artist id in a new array. Then sort that array by increase value and recreate the list of artists in the correct order. Issue is this causes a timeout error on my application as the iteration happens upon clicking “search”.
Method to calculate difference:
class Update < ApplicationRecord
belongs_to :artist
def self.pop_diff(a)
in_range = joins(:artist).where(artists: {name: a}).where(created_at: 30.days.ago.to_date..Time.now().to_date)
diff = in_range.last.popularity - in_range.first.popularity
return diff
end
end
Creating a new array in controller with correct ordering:
#artists = Artist.all
#ordering = Array.new
#artists.each do |a|
#ordering << {"artist" => a, "diff" => Update.pop_diff(a) }
end
#ordering = #ordering.sort_by { |k| k["diff"]}.reverse!
Does anyone know best practice on dealing with these types of situations?
These are the three paths I can think of:
Tweaking above solution to work more efficiently
Using a virtual column (attr_accessor) and storing the increase there. I’ve never done this before, not sure what’s possible
Build a back-end script that saves increase value in database on a daily base.
It would be most performant to do this in SQL
class Artist < ApplicationRecord
def self.get_popularity_extreme(direction = 'ASC', days_ago = 30)
<<-SQL
SELECT popularity
FROM updates
WHERE updates.created_at BETWEEN (DATEADD(DAY, -#{days_ago.to_i.abs}, NOW()), NOW())
ORDER BY updates.created_at #{direction.presence || 'ASC'}
LIMIT 1
SQL
end
def self.by_popularity_difference
joins(
<<-SQL
LEFT JOIN (
#{get_popularity_extreme}
) earliest_update ON updates.artist_id = artists.id
LEFT JOIN (
#{get_popularity_extreme('DESC')}
) latest_update ON updates.artist_id = artists.id
SQL
).
where('earliest_update.popularity IS NOT NULL').
where('latest_update.popularity IS NOT NULL').
select('artists.*', 'latest_update.popularity - earliest_update.popularity AS popularity_difference').
order('popularity_difference DESC')
end
end
Of course this is not the 'rails way'
The other option I would take would be to add a trigger to Update after_save to also set a column in the parent artist table
class Update < ApplicationRecord
belongs_to :artist
after_save :set_artist_difference
def self.pop_diff(a)
in_range = where(artist_id: a.id).where(created_at: 30.days.ago.to_date..Time.now().to_date).limit(1)
in_range.order(created_at: :desc).first.popularity - in_range.order(:created_at).first.popularity
end
def set_artist_difference
artist.update(difference: self.class.pop_diff(a))
end
end
the downside to this is if not every artist gets an update every day, the number won't be accurate
If you are to continue using your current solution, you should specify the order, explicit return is unnecessary, you shouldn't lookup an artist you already have, and the join isn't needed, (and also it's just wrong because your passing the whole artist, yet filtering it on 'name'):
class Update < ApplicationRecord
belongs_to :artist
def self.pop_diff(a)
in_range = where(artist_id: a.id).where(created_at: 30.days.ago.to_date..Time.now().to_date).limit(1)
in_range.order(created_at: :desc).first.popularity - in_range.order(:created_at).first.popularity
end
end
also instead of sorting the opposite direction then reversing, sort by negative diff:
#artists = Artist.all
#ordering = Array.new
#artists.find_in_batches do |batch|
batch.each do |a|
#ordering << {"artist" => a, "diff" => Update.pop_diff(a) }
end
end
#ordering = #ordering.sort_by { |k| -(k["diff"])}
Well, this approach you took has a problem with slow performance, in part because of the many queries you execute in the DB. Here's a simple way to do that (or very close to):
artists = Artist.all
pops =
artists.
includes(:updates).
where('updates.created_at' => 30.days.ago..Time.zone.now).
pluck(:id, 'updates.popularity').
group_by {|g| g.first}.
flat_map do |id, list|
diffs = list.map(&:second).compact
{
artist: artists.find { |artist| artist.id == id},
pops: diffs.last - diffs.first
}
end
# => [{:artist=>#<Artist id: 1, name: "1", created_at: "2018-07-10 05:44:29", updated_at: "2018-07-10 05:44:29">, :pops=>[10, 11, 1]}, {:artist=>#<Artist id: 2, name: "2", created_at: "2018-07-10 05:44:32", updated_at: "2018-07-10 05:44:32">, :pops=>[]}, {:artist=>#<Artist id: 3, name: "3", created_at: "2018-07-10 05:44:34", updated_at: "2018-07-10 05:44:34">, :pops=>[]}]
Much much more performant! But notice this is still not the most performant way to do the job. Still, it is very quick (although a little bit algebraic - you can improve somewhat) and uses a lot of the ruby and rails tricks to achieve the result you're looking for. Hope it helps! =)

Issue with check times overlaps

There are two models:
# Table name: activities_people
#
# activity_id :integer not null
# person_id :integer not null
# date :date not null
# id :integer not null, primary key
# == Schema Information
#
# Table name: activities
#
# id :integer not null, primary key
# name :string(20) not null
# description :text
# active :boolean not null
# day_of_week :string(20) not null
# start_on :time not null
# end_on :time not null
Relations:
activity.rb
has_many :activities_people
has_many :people, through: :activities_people
activity_people.rb
belongs_to :person
belongs_to :activity
I try to create validation that person can join to one activity taking place in specific date and time(start_on, end_on). If I will try sign up to another activity while before I joined to other exercises(same date, and times overlap) should throw error.
What I try:
def check_join_client
activities_date = person.activities_people.where('date = date', date: date)
if activities_date.exists && person.activities.where('id IN (?)', activities_date)
end
I don't know how to use create query(person.activities.where ...) to getting person activities related with activies_people. activities_date check if we joined to activities taking place in same date. Second I want get check start_on and end_on.
Thanks in advance.
If I'm understanding you correctly, you want to find the activites_people for a user that match a query by the date array and then raise an error unless an associated activity for those matched activities_people.
Your original code for check_join_client uses if incorrectly:
def check_join_client
activities_date = person.activities_people.where('date = date', date: date)
if activities_date.exists && person.activities.where('id IN (?)', activities_date)
end
To translate this to pseudocode, you're essentially saying:
result = query if result.condition_met?
However the if condition (the expression after the if) will be evaluated before you define results. It might be more clear if I show a correct approach:
result = query
return result if result.condition_met?
Now to go back to your question about loading associated records, try something like this:
activities_people_ids_matching_date = person.activities_people
.where(date: self.date)
.pluck(:id)
# use pluck to get an array of ids because that's all that's needed here
# not sure how you're getting the date variable, so I'm assuming self.date will work
# I can use `.any?` to see if the ids list is not empty.
condition_met = activities_people_ids_matching_date.any? &&\
person.activities
.where(activities_people_id: activities_people_ids_matching_date)
.any?
condition_met ? true : raise(StandardError, "my error")
There surely is a way to get this done with one query instead of two, but it seems like where you're at with Ruby concepts it's more important to focus on syntax and core functionality than SQL optimization.
The correct syntax (one of several options) is:
person.activities_people.where(date: date)

Rails 4: How to "Duplicate" or Change State of Records from model 'A' to model 'B'?

I have model Quote and model Invoice.
The database for each of these models have the columns: date, company, product and price.
When a customer approves a quote I would like to turn that quote into a invoice with the same values but with the current date and its own invoice_id.
What code should I include in my Invoice model so that the record "duplicates" or "changes" state?
Thank you
In Quote you can use the following code
after_save :generate_invoice, :if => :approved?
def approved?
# your code to return true or false, this method should return true only one time, handle it carefully.
end
def generate_invoice
Invoice.create!(date: Time.now, company: self.company, product: self.product, price: self.price)
end

Rails 3 :How to compare Datetime.

In my Rails application I have a User model ,Department model, Group model and a Register model.User model has basic user information,
User Model:
id , name
has_and_belongs_to_many :departments , :groups
Department Model:
id,name
has_and_belongs_to_many :users
has_many :registers
Group Model:
id,name
has_and_belongs_to_many :users
has_many :registers
Register Model:
date ,department_id , group_id , one , two , three
belongs_to :department ,:group
Among the Register Model "one" , "two" , "three" are time slots say: 9-12 , 12-3 , 3-6.Every day each user has to mark attendance so that their attendance has to mark against the time slot in Register table. How to mark attendance based on current time with the time slot.
Thanks in advance.
You may need to do
current_time = Time.now
if (9..11).include?(current_time.hour)
# Do A
elsif (12..14).include?(current_time.hour)
# Do B
elsif (15..18).include?(current_time.hour)
# Do C
end
(or)
current_time = Time.now
if (9...12).include?(current_time.hour)
# Do A
elsif (12...15).include?(current_time.hour)
# Do B
elsif (15...18).include?(current_time.hour)
# Do C
end
# I think of the extra "." as 'eating' the last value so you don't have it.
to deal with the overlaps
try this
current_time = Time.now
if (9..12).include?(current_time.hour)
# 9-12
elsif (12..15).include?(current_time.hour)
# 12-3
elsif (15..18).include?(current_time.hour)
# 3-6
end
Might consider creating a bitmap. A 32 bit integer is lightweight and gives you 0-24, one for each hour. Write some functions that can compare, set,... hour ranges.

Show content based on holidays

ruby 1.9.2p290
rails 3.1.1
I have these models:
class Recipe < ActiveRecord::Base
belongs_to :festivity
end
class Festivity < ActiveRecord::Base
has_many :recipes
end
I have the following field in the "recipes" table:
festivity_id
And the following datetime fields in the "festivities" table:
starts_at
ends_at
How to display the content based on festivities dates?
I started think in this static way:
class PagesController < ApplicationController
def home
#recipes_by_festivities = Recipe.where(:festivity_id => 4).all(:order => 'RAND()', :limit => 8)
end
end
For example: In Xmas period (about all december month), I want to show a recipe list with the festivity_id = 1. In Thanksgiving period I wanna just a list of recipes with festivity_id = 4.
Am I clear? Let me know if I not.
SOLUTION
#current_festivity = Festivity.find(:last, :conditions => ["? between starts_at AND ends_at", Time.utc(0,Time.now.month,Time.now.day,0,0,0)])
#recipes_by_festivities = Recipe.where(:festivity_id => #current_festivity).all(:order => 'RAND()', :limit => 8)
What I would do is create a "name" field in the festivities table, then use the holidays gem to determine if there's a holiday on a specific date.
Holdays.on(Date.today)
Which will return a list of hashes: each hash has a name key. Then you could use that to look up the correct Festivity object current_festival = Festivies.where(:name => Holdays.on(Date.today)[0][:name]) and from there get the recipes: current_festival.recipes
This assumes starts_at and ends_at are saved with the same year (0), except if the period is in the middle of two years (Christmas time, for example): in that case, the ends_at year must be 1.
Festivity.create(name: 'Christmas time', starts_at: Time.utc(0,12,25,0,0,0), ends_at: Time.utc(1,1,6,23,59,59))
Festivity.create(name: 'Ferragosto', starts_at: Time.utc(0,8,15,0,0,0), ends_at: Time.utc(0,8,15,23,59,59))
Recipe.create(festivity: Festivity.find_by_name('Periodo natalizio'))
Recipe.create(festivity: Festivity.find_by_name('Ferragosto'))
# Searching for festivities today
Recipe.includes(:festivity).where(['? BETWEEN festivities.starts_at AND festivities.ends_at', Time.utc(0,Time.now.month,Time.now.day,12,0,0)]).all
# Searching for festivities on 15 August (in Italy there is a festivity that is called Ferragosto)
Recipe.includes(:festivity).where(['? BETWEEN festivities.starts_at AND festivities.ends_at', Time.utc(0,8,15,12,0,0)]).all
# Searching for festivities on 28 December
Recipe.includes(:festivity).where(['? BETWEEN festivities.starts_at AND festivities.ends_at', Time.utc(0,12,28,12,0,0)]).all
And this is a possible implementation of a function:
def recipes_on(day, month)
Recipe.includes(:festivity).where(['? BETWEEN festivities.starts_at AND festivities.ends_at', Time.utc(0,month,day,12,0,0)]).all
end
Since you want to find recipes by festivities, you can do this:
def home
festivity_today = some_custom_festivity_lookup(Time.now)
if !festivity_today.nil?
#recipes = Recipe.where(festivity_id: festivity_today.id).limit(8)
else
#recipes = Recipe.limit(8)
end
end

Resources