Update rankings/priorities at one go in Ruby on Rails - 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

Related

create nested attributes with conditions

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).

calling self.class.where() in a transaction, but the records i'm trying to select are ignored

I'm using an ActiveRecord::Base.transaction to make a whole bunch of calls pertaining to a grouping of objects that must update/create simultaneously or not at all. One method in this transaction is supposed to use where to find and delete all Trades that match certain parameters.
class Trade < ActiveRecord::Base
include Discard::Model
belongs_to :trade_requester, class_name: "User"
belongs_to :wanted_share, class_name: "Share"
validates :trade_requester, :wanted_share, presence: true
validates :amount, presence: true
def new_wanted_total
wanted_share.amount - amount
end
def update_wanted_share_amount(new_wanted_total)
wanted_share.update_attribute(:amount, new_wanted_total)
end
def delete_extraneous_wanted_trades(wanted_share)
self.class.where("wanted_share_id = ? AND amount > ? AND approved_at = ? AND discarded_at = ?", wanted_share.id, new_wanted_total, nil, nil).delete_all
end
def accept
ActiveRecord::Base.transaction do
delete_extraneous_wanted_trades(wanted_share)
update_wanted_share_amount(new_wanted_total) if new_wanted_total >= 0
Share.create(user_id: self.trade_requester.id, item_id: self.wanted_share.item.id, amount: self.amount, active: false)
self.touch(:approved_at)
end
end
end
When I accept and check the output in my terminal, one line I get says this:
SQL (0.3ms) DELETE FROM "trades" WHERE (wanted_share_id = 8 AND amount > 25 AND approved_at = NULL AND discarded_at = NULL).
I am passing the correct information to the method, and the rest of the terminal output shows that the related records have been updated with the appropriate attributes (one Share set to amount:25 and another Share created with amount:50). But then I check my database, and it says that there is still one Trade for amount: 60. This record exceeds the available total, which is now 50 (it was previously 75), and should have been deleted. But according to the terminal output, it was ignored. Why did this record go untouched?
approved_at = ? AND discarded_at = ? in the where clause should be approved_at IS NULL AND discarded_at IS NULL

Method for greater than one?

I have a query that I run in Rails:
me = User.find(1)
my_groups = me.groups
my_groups can return more than one row, potentially.
Is there a quick and dirty way to use a method to determine if my_groups or me.groups is greater than one?
Maybe something like my_groups.greater_than_one? If not, what would you recommend in determining if the query is return >1 row?
me.groups is essentially another table that is associated with User. It basically shows what "groups" a particular user belongs to.
There needn’t be a method for everything, you can simply compare against size:
me.groups.size > 1
However, ActiveRecord::Relation does have many? which will return true if there is more than one record. From the docs:
Returns true if the collection has more than one record. Equivalent to
collection.size > 1.
class Person < ActiveRecord::Base
has_many :pets
end
person.pets.count #=> 1
person.pets.many? #=> false
person.pets << Pet.new(name: 'Snoopy')
person.pets.count #=> 2
person.pets.many? #=> true
If you only cared about if there are any elements (i.e. >0) there’s any (which is also part of Ruby core’s Enumerable). But beware [nil, false].any? #=> false.
You can get this by:
if me.groups.count > 1 # or me.groups.size > 1 or me.groups.any?
'bla bla...'
else
....
end
But I do recommend to have counter cache in User class.
To do so:
Add a column groups_count to users table
add_column :users, :groups_count, :integer, default: 0
In Group model
belongs_to :user, counter_cache: true
Thus you can achieve your goal by:
if me.groups_count > 1
'bla bla...'
else
....
end
This will reduce the db query

What is the best syntax for using a `where` statement to find association counts

I can think of a few ways to do this, but I'm unsure what to choose..
I have the class Topic and I am trying to scope this so that I only return Topics if it has the associated object Reply or topic.replies as a count greater than 0.
Worst way to do this :
#topics.select{ | topic | topic.replies > 0 && topic.title == "Conversation" }
Ideally, I'd like to use a where scope.
scope = current_user.topics
scope = scope.joins 'left outer join users on topics.registration_id = registration_members.registration_id'
# scope = .. here I want to exclude any of these topics that have both the title "Conversations" and replies that are not greater than 0
I need to "append" these selections to anything else already selected. So my selection shouldn't exclude all others to just this selection. It's just saying that any Topic with replies less than one and also called "Conversation" should be excluded from the final return.
Any ideas?
Update
Sort of a half-hashed idea :
items_table = Arel::Table.new(scope)
unstarted_conversations = scope.select{|a| a.title == "Conversation" && a.replies.count > 0}.map(&:id)
scope.where(items_table[:id].not_in unstarted_conversations)
You can use something called count cache, basically what it does is add a field to the table and store in that field the total of "associates" of the specified type and is automatically updated.
Checkout this old screen/ascii cast: http://railscasts.com/episodes/23-counter-cache-column?view=asciicast
Here is something newer: http://hiteshrawal.blogspot.com/2011/12/rails-counter-cache.html
In your case would be as follow:
# migration
class AddCounterCacheToTopìc < ActiveRecord::Migration
def self.up
add_column :topics, :replies_count, :integer, :default => 0
end
def self.down
remove_column :topics, :replies_count
end
end
# model
class Replay < ActiveRecord::Base
belongs_to :topic, :counter_cache => true
end
Hope it help you.

Rails find conditions... where attribute is not a database column

I think it's safe to say everyone loves doing something like this in Rails:
Product.find(:all, :conditions => {:featured => true})
This will return all products where the attribute "featured" (which is a database column) is true. But let's say I have a method on Product like this:
def display_ready?
(self.photos.length > 0) && (File.exist?(self.file.path))
end
...and I want to find all products where that method returns true. I can think of several messy ways of doing it, but I think it's also safe to say we love Rails because most things are not messy.
I'd say it's a pretty common problem for me... I'd have to imagine that a good answer will help many people. Any non-messy ideas?
The only reliable way to filter these is the somewhat ugly method of retrieving all records and running them through a select:
display_ready_products = Product.all.select(&:display_ready?)
This is inefficient to the extreme especially if you have a large number of products which are probably not going to qualify.
The better way to do this is to have a counter cache for your photos, plus a flag set when your file is uploaded:
class Product < ActiveRecord::Base
has_many :photos
end
class Photo < ActiveRecord::Base
belongs_to :product, :counter_cache => true
end
You'll need to add a column to the Product table:
add_column :products, :photos_count, :default => 0
This will give you a column with the number of photos. There's a way to pre-populate these counters with the correct numbers at the start instead of zero, but there's no need to get into that here.
Add a column to record your file flag:
add_column :products, :file_exists, :boolean, :null => false, :default => false
Now trigger this when saving:
class Product < ActiveRecord::Base
before_save :assign_file_exists_flag
protected
def assign_file_exists_flag
self.file_exists = File.exist?(self.file.path)
end
end
Since these two attributes are rendered into database columns, you can now query on them directly:
Product.find(:all, :conditions => 'file_exists=1 AND photos_count>0')
You can clean that up by writing two named scopes that will encapsulate that behavior.
You need to do a two level select:
1) Select all possible rows from the database. This happens in the db.
2) Within Ruby, select the valid rows from all of the rows. Eg
possible_products = Product.find(:all, :conditions => {:featured => true})
products = possible_products.select{|p| p.display_ready?}
Added:
Or:
products = Product.find(:all, :conditions => {:featured => true}).select {|p|
p.display_ready?}
The second select is the select method of the Array object. Select is a very handy method, along with detect. (Detect comes from Enumerable and is mixed in with Array.)

Resources