How to restructure results from database - ruby-on-rails

I've got a model User which has id, name, surname, role_id, permission_id
Here is few user entries :
User => {:name => 'Bob', :surname => 'surnmae', :role_id => 1, :permission_id = 2}
User => {:name => 'Alice', :surname => 'strange', :role_id => 1, :permission_id = 3}
User => {:name => 'Ted', :surname => 'Teddy', :role_id => 2, :permission_id = 3}
Now I need to group them first by role_id and then by permission_id, here is what I mean :
Category.select([:name, :role_id, :permission_id]).group_by(&:role_id)
Produces :
{1 =>
[#<User name: "Bob", role_id: 1, permission_id: 2>,
#<User name: "Alice", role_id: 1, permission_id: 3>]
2 => [#<User name: "Ted", role_id: 2, permission_id: 3>]
}
Which is close enough but I need them grouped with permission_id as well so it would look like this :
{:role_id => {:permision_id => [Array of users grouped by this criteria]}
}

It is bit more tricky than it seems. It is better to write it into multiple steps. However, the following one-liner will work:
Hash[ User.all.group_by(&:role_id).collect{|role, grp| [role, grp.group_by(&:permission_id)]} ]
Output will be the following (which is probably what you are looking for):
{1=>
{2=>[{:name=>"Bob", :surname=>"surnmae", :role_id=>1, :permission_id=>2}],
3=>[{:name=>"Alice", :surname=>"strange", :role_id=>1, :permission_id=>3}]},
2=>{3=>[{:name=>"Ted", :surname=>"Teddy", :role_id=>2, :permission_id=>3}]}}
Same logic, but simpler to comprehend:
output={}
User.all.group_by(&:role_id).each{|role, grp| output[role]= grp.group_by(&:permission_id)}
# output has required goruping

maybe something like
Category.select([:name, :role_id, :permission_id]).group_by(&:role_id).map{|role| role.group_by(&:permission_id)}

Category.select([:name, :role_id, :permission_id]).group_by { |category| [category.role_id, category.permission_id] }
Edit: Nevermind, this doesn't provide quite the formatting you're looking for.

Related

Ruby how to format nested hash

I have two queries I am running and iterating over both and my final hash is seen below. But, I want to have format on how the data is being stored in the hash that I'm creating or format it after I'm done creating it. But I am not sure how to achieve the desired format where the names fall under the same id as show below
desired format of example data:
[
{
id: 1,
accepted: false,
trans: 10234
names: [
{ name: "Joe", amount: "$1,698.00" },
{ name: "Smith", amount: "$674.24" },
]
},
{
id: 2,
accepted: true,
trans: 10234,
names: [
{ name: "Joe", amount: "$1,698.00" },
{ name: "Smith", amount: "$674.24" },
]
}
]
current format I have
[
{
:id => 1,
:accepted => false,
:trans => 8,
:name => "Smith",
:amount => 36.0
},
{
:id => 1,
:amount => false,
:trans => 8,
:name => "Joe",
:amount => 6.0
},
{
:id => 3,
:accepted => false,
:trans => 8,
:name => "Tom",
:amount => 34.0
},
{
:id => 3,
:accepted => false,
:trans=> 8,
:name => "Martha",
:amount => 4.0
}
],
[
{
:id => 2,
:accepted => true,
:trans => 7,
:name => "Bob",
:amount => 35.0
},
{
:id => 2,
:accepted => true,
:trans => 7,
:name => "John",
:amount => 5.0
}
]
logic for creating hash
imports = ListImports.limit(20).order(created_at: :DESC)
groups = imports.map{|import| ListImportGroup.where(list_import_id: import.id)}
pub_hash_true = []
pub_hash_false = []
hash = []
imports.map do |import|
hash << {
id: import.id,
trans: import.trans,
accepted: import.amount
}
end
hash.each do |import|
groups.flatten.each do |group|
accepted = import[:accepted]
num_transactions = import[:trans]
if accepted == false
pub_hash_false << {id: import[:id], accepted: accepted, trans: num_transactions, name: group.name, amount: group.amount}
else
pub_hash_true << {id: import[:id], accepted: accepted, trans: num_transactions, name: group.name, amount: group.amount}
end
end
end
# Note: You didn't specify what is the association between `ListImport` and `ListImportGroup`.
# However, I'm fairly sure you could be fetching this data via a JOIN query like below,
# rather than making up to 20 additional database calls to fetch the associated records.
imports = ListImports.limit(20).order(created_at: :DESC).includes(:list_import_group)
result = imports.map do |import|
{
id: import.id,
trans: import.trans,
accepted: import.amount,
names: import.list_import_groups.pluck(:name, :amount)
}
end
And if you do actually need to filter for imports where accepted is true or false, you could do something like this instead of building separate arrays manually:
accepted_imports = result.select { |import| import[:accepted] }
# and
rejected_imports = result.reject { |import| import[:accepted] }
# or even:
accepted_imports, rejected_imports = result.partition { |import| import[:accepted] }
You didn't specify the exact correspondence between the desired and current formats.
But I assume
For the entries with the same id, the values of accepted and trans are identical.
the desired amount for Joe in the current format is identical in the corresponding amount in the desired amount. (In your example, the former is 6.0 whereas the latter is "$1,698.00", which does not make sense.)
Then, the following would do the conversion. The array ahout is in the desired format.
# Let us assume "a1" is the original array in the "current format"
hout = {}
a1.flatten.map{|h|
h.slice(*(%i(id trans name amount accepted))).values
}.each{ |a|
hout[a[0]] = {id: a[0], accepted: a[4], trans: a[1], names: []} if !hout.key? a[0]
hout[a[0]][:names].push(
{name: a[2], amount: "$"+helper.number_with_precision(a[3], precision: 2, delimiter: ',')}
)
}
ahout = hout.values
You may want to sort ahout, if you like.
Note that I am assuming you are using Rails 5+. Otherwise, the method helper may not work. In that case, you can use sprintf or whatever formatting method.

Using group by and sort by in nested hash

I have a function:
tracks.group_by { |t| t[:track].track_category }
Which creates a hash:
{
#<TrackCategory id: 2, order: 0> =>
[{:track =>
#<Track promo_order: 2>,
:order => 2},
{:track =>
#<Track promo_order: 2>,
:order => 1}],
#<TrackCategory id: 1, order: 1> =>
[{:track =>
#<Track promo_order: 2>,
:order => 2},
{:track =>
#<Track promo_order: 2>,
:order => 1}]
}
I've been trying to sort the TrackCategories by order, and inside that sort the Tracks by promo_order.
This keeps giving me errors, and I'm positive I'm doing something wrong
tracks.group_by { |t| t[:track].track_category }.sort_by { |t| t[:order] }.sort_by { |t| t[:promo_order] }
You are passing a key-value pair which came from group_by and was coerced as an array to sort_by, and then trying to access a key in the array, which of course doesn't exist.
In other words, you're mistaking the input used in sort_by. In the first sort_by, |t| is a hash key-pair generated by group_by and coerced into a array of the form [k,v]. In your case, t is:
[ #<TrackCategory id: 2, order: 0>,
[{:track =>
#<Track promo_order: 2>,
:order => 2},
{:track =>
#<Track promo_order: 2>,
:order => 1}],
#...
]
So what you want is to decompose the array in its elements so you can get the ones you want, something like:
tracks.group_by { |t| t[:track].track_category }
.sort_by { |t, _| t[:order] } # here you're sorting by TrackCategory#[:order]
And something similar on the second line, but using [:promo_order] instead.

How to configure decimal_mark and thousand_separator - Money Rails

I'm trying use the gem money-rails but with no success.
I live in Brazil and here the decimal mark is "," and the thousand separator is "."
So, I add the follow code in a money.rb initializer:
MoneyRails.configure do |config|
config.default_currency = :brl
config.register_currency = {
:id => :brl,
:priority => 1,
:iso_code => "BRL",
:name => "Real",
:symbol => "R$",
:symbol_first => true,
:subunit => "Cent",
:subunit_to_unit => 100,
:thousands_separator => ".",
:decimal_mark => ","
}
end
And in my model class "Produto":
class Produto < ApplicationRecord
attr_accessor :nome, :preco
monetize :preco_centavos
end
But when I try use this in Rails console I get a different behavior:
irb(main):001:0> p = Produto.new
=> #<Produto id: nil, nome: nil, preco_centavos: 0, preco_currency: "BRL", created_at: nil, updated_at: nil>
irb(main):002:0> p.preco = 1000.to_money
=> #<Money fractional:100 currency:BRL>
irb(main):003:0> p.preco.format
=> "R$1,000.00"
The format method return "R$1,000.00" when I expect "R$1.000,00".
Someone already passed for this?
PS: Sorry for my bad English
This is a bug (or at least a misleading feature) in the money-rails gem code. Set Money.use_i18n = false. Once you do this, then you won't need to do config.register_currency for the commas/decimals, at least. 1000.to_money('BRL').format yields "R$1.000,00".
See https://github.com/RubyMoney/money-rails/blob/4a35ae823843fba80c036e2332e80d6b9e06dcb5/lib/money-rails/active_model/validator.rb#L58 for why this is the case.

Validates_Overlap Gem Multiple Scopes Overwriting Eachother

I'm using Validates_Overlap Gem which can be found here: https://github.com/robinbortlik/validates_overlap
The essence is I have two rooms that can be booked. I want the validation to step in when the same room already has a CONFIRMED booking in the SAME room. It shouldn't throw me an error when the other room is booked, or if the same room is booked but hasn't been confirmed.
My code so far is as follows
validates :start_time, :end_time,
:overlap => {
:exclude_edges => ["starts_at", "ends_at"],
:scope => { "bookings.studio_id" => proc {|booking| booking.studio_id}} && { "bookings.is_confirmed" => proc {|booking| booking.is_confirmed == true}}
}, on: :update
This returns the following from my server:
Booking Exists (0.4ms) SELECT 1 AS one FROM "bookings" WHERE ((bookings.end_time IS NULL OR bookings.end_time >= '2014-10-23 20:00:00.000000') AND (bookings.start_time IS NULL OR bookings.start_time <= '2014-10-24 03:00:00.000000') AND bookings.id != 9 AND bookings.is_confirmed = 't') LIMIT 1
There are two other bookings (with this studio_id) and none of them are confirmed. What gives?
Here are all the bookings with :studio_id => 2
[#<Booking id: 1, studio_id: 2, engineer_id: 5, is_confirmed: false, title: "", allDay: false, created_at: "2014-10-23 19:59:01", updated_at: "2014-10-23 19:59:01", start_time: "2014-10-23 19:00:00", end_time: "2014-10-23 21:00:00", user_id: nil, booker: "Client", client_id: 3>,
#<Booking id: 8, studio_id: 2, engineer_id: 1, is_confirmed: false, title: "", allDay: false, created_at: "2014-10-24 03:07:34", updated_at: "2014-10-24 03:07:34", start_time: "2014-10-23 19:00:00", end_time: "2014-10-23 22:00:00", user_id: nil, booker: "Pat Sullivan", client_id: nil>,
#<Booking id: 9, studio_id: 2, engineer_id: 2, is_confirmed: false, title: "", allDay: false, created_at: "2014-10-24 03:26:17", updated_at: "2014-10-24 03:26:17", start_time: "2014-10-23 20:00:00", end_time: "2014-10-24 03:00:00", user_id: nil, booker: "Client", client_id: 4>]
Update I noticed that the studio_id isn't being noticed with the && in the scope line. How can I have the scope register both? Can I do it within the scope line or should I create a method?
I've also tried a simpler
validates :start_time, :end_time,
:overlap => {
:exclude_edges => ["starts_at", "ends_at"],
:scope => "is_confirmed" && "studio_id"
}, on: :update
This does the same thing -- only uses the later "studio_id"
I know, that the names of options are confusing and I'm sorry for that.
I suggest you to implement your named scope called :confirmed and pass it as :query_option parameter.
I think, it should look like this:
class Booking < ActiveRecord::Base
scope :confirmed_scope, -> {confirmed: true}
validates :start_time, :end_time, :overlap => {
:exclude_edges => ["starts_at", "ends_at"],
:scope => "studio_id",
:query_options => {:confirmed_scope => nil}
}, on: :update
end
BTW... be careful if you are using Rails 4.1, there is a change https://github.com/robinbortlik/validates_overlap#rails-41-update
Short explanation: what you pass as a :scope option, this behave like attribute. But you can extend it by :query_options. What is inside query options will be called in the query chain. So internally it will be called like this:
Booking.confirmed_scope.where("starts_at > 18-02-2014 AND ends_at < 20-02-2014 AND studio_id = 1")
Is it more clear now?

How would I save multiple records at once in Rails?

How would I save this array in one call with Rails?
tax_rates = [{
:income_from => 0
:income_to => 18200
:start => "01-07-2013"
:finish => "30-06-2014"
:rate => nil
:premium => nil
},{
:income_from => 18201
:income_to => 37000
:start => "01-07-2013"
:finish => "30-06-2014"
:rate => 0.19
:premium => nil
},{
:income_from => 18201
:income_to => 37000
:start => "01-07-2013"
:finish => "30-06-2014"
:rate => 0.19
:premium => nil
}]
Can I just call Rails.create(tax_rates)?
Also, is there a way to remove duplicate symbols so they look neater?
Your example is almost correct.
Use ActiveRecord::Persistence#create, which can accept an array of hashes as a parameter.
tax_rates = [
{
income_from: 0,
income_to: 18200,
start: "01-07-2013",
finish: "30-06-2014",
rate: nil,
premium: nil,
},
{
income_from: 18201,
income_to: 37000,
start: "01-07-2013",
finish: "30-06-2014",
rate: 0.19,
premium: nil,
},
# ...
]
TaxRate.create(tax_rates) # Or `create!` to raise if validations fail
A nice solution is to use the active record import gem. I recommend it over now built-in Rails bulk insert because it's more flexible in the options in case of constraint violation.
TaxRate.import(
[:income_from, :income_to, :start, :finish, :rate, :premium],
tax_rates
)
Its definitely better than my old answer which would trigger a db commit per entry in the array :)
Old answer:
tax_rates.map {|tax_rate| TaxRate.new(tax_rate).save }
This way you'll retrieve an Array with true or false to know which did succeed and which didn't.
If you want all of them to be saved .or, non of them to be saved even if one fails, you can use 'ActiveRecord::Base.transaction'
e.g.
ActiveRecord::Base.transaction do
tax_rate.each do |tax_rt|
TaxRate.new(tax_rt).save
end
end
I am not sure about rails < 4.2 but I have tried it in rails 4.2 you can simply do this
TaxRate.create(tax_rt)
Here is an example like yours:
a = []
a << B.new(:name => "c")
a << B.new(:name => "s")
a << B.new(:name => "e")
a << B.new(:name => "t")
The array is saved all at once with:
a.each(&:save)
This will call B#save on each item in the array.
use a gem 'fast_inserter': https://github.com/joinhandshake/fast_inserter
it generates a single sql query of thousand records.
movie_data = [1, 'Climates (Iklimler)', 'Clay Pauwel', 'Drama'],
[2, 'Tinpis Run', 'Andros Glazer', 'Comedy'],
[3, 'Naked City, The', 'Bethena Chatband', 'Mystery'],
[4, 'Small Time Crooks', 'Naomi Plom', 'Crime'],
[5, 'Shadowboxer', 'Georgeanne Widdicombe', 'Thriller']
params = {
table: 'movies',
static_columns: {
created_at: '0000-00-00 00:00:00',
updated_at: '0000-00-00 00:00:00',
},
options: {
timestamps: false,
unique: true,
check_for_existing: true
},
group_size: 100,
variable_columns: %w(id title director description),
values: movie_data
}
inserter = FastInserter::Base.new(params)
inserter.fast_insert

Resources