create nested attributes with conditions - ruby-on-rails

I have three models Restocking, Product, and Size
#Product
has_many :sizes, as: :sizeable
#Size
belongs_to :sizeable, polymorphic: true
restocking.rb
class Restocking < ApplicationRecord
has_many :sizes, as: :sizeable
belongs_to :product
accepts_nested_attributes_for :sizes
def update_existing_product
product = self.product
product.update_attributes(
price: self.price,
buying_price: self.buying_price,
)
sizes = Size.where(sizeable_id: self.product_id)
self.sizes.each do |restocking_size|
sizes.each do |product_size|
if product_size.size_name == restocking_size.size_name
product_size.quantity += restocking_size.quantity
product_size.save
end
end
end
end
end
So the method update_existing_productupdate prices and quantity of existing sizes...
If a similar size_name is found it updates the existing size quantity otherwise it creates a new one...
I don't manage to correctly create new sizes...
I am supposed to use this Size.create method, but when I put it on the loop it creates the same size many times.
Size.create!(
sizeable_id: self.product_id,
sizeable_type: "Product",
size_name: restocking_size.size_name,
quantity: restocking_size.quantity,
)

Size is created many times because of how your loop is constructed.
Fix for your code can look like this:
self.sizes.each do |restocking_size|
if (existing_size = sizes.find{|s| s.size_name == restocking_size.size_name })
existing_size.tap{|s| s.quantity += restocking_size.quantity }.save!
else
# no existing, here goes create
end
end
But keep in mind, that handling this at application level can lead to race conditions, if this code happens to run at the same time when some other code updates same data.
For example:
we have 10 items of size A of item B
restoking another 5
code runs, fetches sizes, there we have 10 in quantity
at this moment someone buys one item of that size, 9 items left and is this written to db
restocking continues to run - adds 5 to 10, writes 15 to db
quantity is 15, while one item has been sold
Can be avoided by using record#with_lock{ here update happens } in every place where you update counter (but this reloads the record, can be inefficient for large volumes).

Related

Ruby do-loop to break down an array/hash

I made a self referring database using the has_many :through relationship:
**Product**
name
**Ingredient**
quantity
product_id
product_component_id
I can have an egg, carton of 12 eggs, and a flat of 16 cartons.
I am trying to write a loop that starts with a product and breaks down all the components of each product and those to the most basic state. The goal is to return an array of all the base products that go into any given product so the carton would return 12 eggs and the flat would return 192 Eggs.
I gave it a shot and this is how far I got:
def product_breakdown
results = []
ingredients.each do |ingredient|
if ingredient.product_component_id == nil
results += ingredient
else
Keep digging deeper?
end
end
return results
end
I am missing a whole concept when it comes to using the loop. If anyone has an advise on the name of the concepts that this requires, I would be very appreciative.
edit in order to be more clear I copied the relationships of the database.
class Product < ActiveRecord::Base
has_many :ingredients
has_many :product_components, :through => :ingredients
end
class Ingredient < ActiveRecord::Base
belongs_to :product
belongs_to :product_component, class_name: "Product", :foreign_key => "product_component_id"
end
I suggest using each_with_object to build the array. That way you don't even need the results variable, just return each_with_object's return value.
How do you differentiate between a unit, carton, and flat?
If I understand correctly, each ingredient has a component which can be nil, Carton, or Flat? And one carton always contains 12 units, and one flat 16 cartons? And a source, which is the type of ingredient (egg, milk, etc?)
In that case, I'd define a couple helper methods on Ingredient, an as_unit class method and a unit_quantity instance method:
def unit_quantity
case product_component_id
when nil
quantity
when CARTON_COMPONENT_ID
12 * quantity
when FLAT_COMPONENT_ID
192 * quantity
end
end
def self.as_unit ingredients
source_ids = ingredients.map(&:product_source_id).uniq
raise "Can't join different types together" if source_ids.count != 1
source_id = source_ids.first
quantity = ingredients.reduce(0) { |total, ingredient| total += ingredient.unit_quantity }
Ingredient.new quantity: quantity, product_component_id: nil, product_source_id: source_id
end
That way, you can rewrite products_breakdown to be:
def products_breakdown ingredients
ingredients.group_by(&:product_source_id).map do |_, ingredients|
Ingredient.as_unit ingredients
end
end
This should result in:
$ ingredients
#=> [<Ingredient: 3 Cartons of Egg>, <Ingredient: 2 Flats of Milk>, <17 Units of Egg>]
$ product_breakdown ingredients
#=> [<Ingredient: 53 Units of Egg>, <Ingredient: 384 Units of Milk>]
Is this at all what you were looking for? I'm not sure I fully understood your question...

Rails: Joining the same table twice and querying a joined attribute

I have a Rails app that parses price information from a variety of sources. The models look something like this:
class Price
attr_accessor :value
belongs_to :product
belongs_to :added_pricelist, :class_name => "Scrape"
belongs_to :removed_pricelist, :class_name => "Scrape"
end
class Product
attr_accessor :name
end
class PriceList
attr_accessor :created_at
has_many :prices
end
Every day, a scraper runs, and parses some prices for products from an API. It creates a new PriceList every time the scraper is run. The scraper records which pricelist a price appeared (or disappeared) in - so the data might look like this.
# NB: Assume product_id is always 1 for these
{ value: 100, added_pricelist_id: 1, removed_pricelist_id: 2 }
{ value: 120, added_pricelist_id: 2, removed_pricelist_id: 3 }
{ value: 140, added_pricelist_id: 3, removed_pricelist_id: 4 }
A new object is only created when the price changes, so if it stayed the same, you'd just have:
{ value: 100, added_pricelist_id: 1, removed_pricelist_id: 4 }
My question is: Using ActiveRecord, how can I find the average price of a product for each of the last 30 days? I need to be able to find all the prices that were active on a particular day, and then average that number. Can that be done in a single query? (NB: This is with Postgres).
Feel free to get me to clarify anything that doesn't make any sense, and thanks in advance for helping!
AVG
Although I was a little confused with what you posted, hopefully I can give you some help:
You have at least two options:
ActiveRecord .average
SQL AVG
Both of these perform the same task (except one is ActiveRecord based), so I would look at firstly getting your SQL Query correct, and then performing the AVG function on the data you have
Query
I need to be able to find all the prices that were active on a
particular day, and then average that number
I would do something like this:
#app/models/PriceList.rb
Class PriceList < ActiveRecord::Base
scope :today, -> { where(created_at: DateTime.now.at_beginning_of_day.utc..Time.now.utc).average(:value) }
end
This would allow you to call #product.pricelist.today

Update rankings/priorities at one go in Ruby on Rails

I am working on a personal project in Ruby on Rails in which I have a song model which has a field called priority. A user can add songs to a playlist and set priority to each song, which determines the order in which the songs are played. The problem I am stuck with here, is that I need unique priorities.
So if I have a playlist with 8 songs, and I am adding song number 9 with priority 4, the remaining songs should get their priorities updated. Can any one suggest what is the best way of going about this ?
I'm actually tasked with a similar problem in my own application.
Here's my first take:
class Playlist
has_many :playlist_songs
has_many :songs, through: :playlist_songs
end
--
class PlaylistSong
before_save :update_priorities, if: :priorities_changed?
def priorities_changed?
new_record? || priority_was != priority
end
def update_priorities
if new_record?
playlist.playlist_songs.update_all("priority = priority+1 where priority >= #{priority}")
else
# ouch. not so simple with unique constraint on priority
end
end
end
The problem with the unique constraint is this:
priorities: 1,2,3
update: 3 -> 1
#> triggers
1 -> 2
2 -> 3
But 3 is already taken (the current record in which we're changing priority to 1). We cannot change to 1 first because 1 is taken. The only idea I have at the minute is to set the currently updated record to a non-unique priority like 999, but then we're affectively locking updates on the priorities between that query and updating the priority list. I guess you could temporarily set it to a large random number to avoid such unlikely conflicts.
See here for why this is particularly tricky:
Reordering an ordered list
Shouldn't you be able to use the validates_uniqueness_of constraint in the Song model to achieve this?
class Song < ActiveRecord::Base
validates :priority, numericality: { only_integer: true }, :uniqueness => {:scope => :playlist_id}
end
This should force the user to type in a different number other than the ones already saved for a particular playlist...
This is the way I dealt with it - if I enforced a uniqueness constraint, I will have to go with the way Damien has dealt - by assigning some unique priority and then updating. So instead, I decided to let my code deal with the uniqueness. Here's what I have done :
While creating a new song for the playlist, I am calling the create_new_priorities_for_existing_songs in the songs#create action -
def self.create_new_priorities_for_existing_songs (song, priority)
song_id = song.id
playlist = song.playlist
all_songs = playlist.songs
all_songs.where("priority >= :priority and id != :song_id", :priority => priority, :song_id => song_id).update_all ("priority = priority+1")
end
I am calling the update_priorities_of_existing_songs method in my songs#update action -
def self.update_priorities_of_existing_songs (song_id, old_priority, new_priority)
return if old_priority == new_priority
song = Song.find(song_id)
playlist = song.playlist
all_songs = playlist.songs
if old_priority < new_priority
all_songs.where("priority > :old_priority and priority <= :new_priority and id != :song_id", :old_priority => old_priority, :new_priority => new_priority, :song_id => song_id).update_all ("priority = priority-1")
elsif old_priority > new_priority
all_songs.where("priority >= :new_priority and priority < :old_priority and id != :song_id", :old_priority => old_priority, :new_priority => new_priority, :song_id => song_id).update_all ("priority = priority+1")
end
end

ActiveRecord group by on a join

Really been struggling trying to get a group by to work when I have to join to another table. I can get the group by to work when I don't join, but when I want to group by a column on the other table I start having problems.
Tables:
Book
id, category_id
Category
id, name
ActiveRecord schema:
class Category < ActiveRecord::Base
has_many :books
end
class Book < ActiveRecord::Base
belongs_to :category
end
I am trying to get a group by on a count of categories. I.E. I want to know how many books are in each category.
I have tried numerous things, here is the latest,
books = Book.joins(:category).where(:select => 'count(books.id), Category.name', :group => 'Category.name')
I am looking to get something back like
[{:name => fiction, :count => 12}, {:name => non-fiction, :count => 4}]
Any ideas?
Thanks in advance!
How about this:
Category.joins(:books).group("categories.id").count
It should return an array of key/value pairs, where the key represents the category id, and the value represents the count of books associated with that category.
If you're just after the count of books in each category, the association methods you get from the has_many association may be enough (check out the Association Basics guide). You can get the number of books that belong to a particular category using
#category.books.size
If you wanted to build the array you described, you could build it yourself with something like:
array = Categories.all.map { |cat| { name: cat.name, count: cat.books.size } }
As an extra point, if you're likely to be looking up the number of books in a category frequently, you may also want to consider using a counter cache so getting the count of books in a category doesn't require an additional trip to the database. To do that, you'd need to make the following change in your books model:
# books.rb
belongs_to :category, counter_cache: true
And create a migration to add and initialize the column to be used by the counter cache:
class AddBooksCountToCategories < ActiveRecord::Migration
def change
add_column :categories, :books_count, :integer, default: 0, null: false
Category.all.each do |cat|
Category.reset_counters(cat.id, :books)
end
end
end
EDIT: After some experimentation, the following should give you close to what you want:
counts = Category.joins(:books).count(group: 'categories.name')
That will return a hash with the category name as keys and the counts as values. You could use .map { |k, v| { name: k, count: v } } to then get it to exactly the format you specified in your question.
I would keep an eye on something like that though -- once you have a large enough number of books, the join could slow things down somewhat. Using counter_cache will always be the most performant, and for a large enough number of books eager loading with two separate queries may also give you better performance (which was the reason eager loading using includes changed from using a joins to multiple queries in Rails 2.1).

rails question: how to create multiple child objects linked to a parent object with a select drop down in the parents create form?

In a booking engine for buying tickets for events, written in rails, I have the following models:
an order
a ticket
One order has many tickets, and one ticket belongs to an event.
People that want to book tickets for an event, typically create a new order. They fill in some details (kept in the order model), and at the end they see some drop downs where they can select the number of tickets for each ticket type (for example: VIP tickets, student tickets, ...).
a screenshot can be found here:
http://eerlings.com/orders.png
I would like to implement that when the order is created in the DB, for each ticket type, there is a ticket created in the DB, linked to this order. If the attendee selected "5" in the drop down for the VIP tickets, and "3" for the student tickets, there should be 8 tickets created in the DB in total.
What would be the best way to implement this in rails? any suggestions?
Ciao,
Pieter
You'd probably have separate params for VIP tickets and student tickets, something like
params = {
:num_vip_tickets => 5,
:vip_ticket => {
:event_id => 1,
:price => 250
},
:num_student_tickets => 3,
:student_ticket => {
:event_id => 1,
:price => 10
}
}
I would handle that in the controller. For instance, using some assumed names:
if params[:num_vip_tickets]
params[:num_vip_tickets].to_i.times {#order.tickets.create params[:vip_ticket]}
end
Another way would be
class Order < ActiveRecord::Base
has_many :tickets
accepts_nested_attributes_for :tickets
end
class OrdersController < ApplicationController
def create
params[:order][:ticket_attributes] = []
num_student_tickets = params[:num_student_tickets].to_i
if num_student_tickets > 0
params[:order][:tickets_attributes] += [params[:student_ticket]] * num_student_tickets
end
num_vip_tickets = params[:num_vip_tickets].to_i
if num_vip_tickets > 0
params[:order][:tickets_attributes] += [params[:vip_ticket]] * num_vip_tickets
end
#order = Order.new params[:order]
# ... etc ...
end
end
See this question:
how to have linked drop downs in a rails form

Resources