Comparing two lists and syncronizing the database based on differences - ruby-on-rails

I have the following resource relationship:
Class Course < ActiveRecord::Base
has_many :track_courses
has_many :tracks, through: :track_courses
end
as well as a mirroring relationship inside the Track model. The TrackCourse table which connects these models has these rows:
id: primary key
track_id: represents the track
course_id: represents the course
position: the ordering of the course inside that track
I want to allow admin users to be able to update the courses in each track via ajax. I have a list on the front-end that is being passed to the controller as a hash:
front_end_list = { course_id => position }
which represents the object and its position on the front-end sortable.
I'm also looking up the list of existing courses in that track:
existing_courses = TrackCourse.where("track_id = ?", track_id).all
GOAL: Compare these two lists and syncronize the database entries according to the front-end list. Essentially, if the user inserts Course 15 into position 2 on the webpage, I need to either insert that entry into TrackCourse table (if it doesn't exist) or update its position (if it exists). And vice versa for remove.
What is the best way of doing this? Do ActiveRecord/ActiveRelation provide methods for it? Or do I have to write something myself?
UPDATE: I found a gem called acts_as_list, but it seems to be designed for ActiveRecord tables as opposed to ActiveRelation. It essentially expects position values to be unique, whereas in TrackCourse there can be multiple course with same position (in different tracks).

I figured out a solution. I'll post my code here in case it helps anyone else down the line.
I have this method in my controller that processes the ajax request from the front-end:
def sort
track_id = params[:track_id]
courses_in_list = {}
params[:course].each do |courseid|
position = params[:course].index(courseid)
courses_in_list[courseid.to_i] = position
end
existing_courses_in_track = {}
TrackCourse.where("track_id = ?", track_id).to_a.each do |track_course|
existing_courses_in_track[track_course.course_id] = track_course.position
end
if courses_in_list.length < existing_courses_in_track.length
existing_courses_in_track.each do |courseid, position|
if courses_in_list[courseid].nil?
track_course = TrackCourse.where(track_id: track_id, course_id: courseid).first
track_course.remove_from_list
track_course.destroy!
end
end
else
if existing_courses_in_track.empty?
track_course = TrackCourse.new(track_id: track_id,
course_id: courses_in_list.keys[0])
track_course.insert_at(courses_in_list.values[0])
p "first track!"
else
courses_in_list.each do |courseid, position|
track_exists = false
if !existing_courses_in_track[courseid].nil?
track_course_position = existing_courses_in_track[courseid]
track_exists = true
end
if !track_exists
TrackCourse.new(track_id: track_id, course_id: courseid).insert_at(position)
else
p "else statement"
track_course = TrackCourse.where(track_id: track_id, course_id: courseid).first
track_course.update_attribute(:position, position)
end
end
end
end
render :nothing => true
end
Essentially, I'm building two hashes, one based on the list of front-end items and their position, and one based on the database courses and their position. I then compare them. If the front-end list is shorter, that means the user removed an item, so I iterate through the backend list, find the extra item, and remove it. Then I employ a similar mechanism for adding items to the list and resorting the list. The acts_as_list gem really helps with keeping things in the correct position. However, I did have to limit its scope when I included it in my model to ensure it runs only on relationships (TrackCourses) with a specific track_id.

Related

Rails best way to get previous and next active record object

I need to get the previous and next active record objects with Rails. I did it, but don't know if it's the right way to do that.
What I've got:
Controller:
#product = Product.friendly.find(params[:id])
order_list = Product.select(:id).all.map(&:id)
current_position = order_list.index(#product.id)
#previous_product = #collection.products.find(order_list[current_position - 1]) if order_list[current_position - 1]
#next_product = #collection.products.find(order_list[current_position + 1]) if order_list[current_position + 1]
#previous_product ||= Product.last
#next_product ||= Product.first
product_model.rb
default_scope -> {order(:product_sub_group_id => :asc, :id => :asc)}
So, the problem here is that I need to go to my database and get all this ids to know who is the previous and the next.
Tried to use the gem order_query, but it did not work for me and I noted that it goes to the database and fetch all the records in that order, so, that's why I did the same but getting only the ids.
All the solutions that I found was with simple order querys. Order by id or something like a priority field.
Write these methods in your Product model:
class Product
def next
self.class.where("id > ?", id).first
end
def previous
self.class.where("id < ?", id).last
end
end
Now you can do in your controller:
#product = Product.friendly.find(params[:id])
#previous_product = #product.next
#next_product = #product.previous
Please try it, but its not tested.
Thanks
I think it would be faster to do it with only two SQL requests, that only select two rows (and not the entire table). Considering that your default order is sorted by id (otherwise, force the sorting by id) :
#previous_product = Product.where('id < ?', params[:id]).last
#next_product = Product.where('id > ?', params[:id]).first
If the product is the last, then #next_product will be nil, and if it is the first, then, #previous_product will be nil.
There's no easy out-of-the-box solution.
A little dirty, but working way is carefully sorting out what conditions are there for finding next and previous items. With id it's quite easy, since all ids are different, and Rails Guy's answer describes just that: in next for a known id pick a first entry with a larger id (if results are ordered by id, as per defaults). More than that - his answer hints to place next and previous into the model class. Do so.
If there are multiple order criteria, things get complicated. Say, we have a set of rows sorted by group parameter first (which can possibly have equal values on different rows) and then by id (which id different everywhere, guaranteed). Results are ordered by group and then by id (both ascending), so we can possibly encounter two situations of getting the next element, it's the first from the list that has elements, that (so many that):
have the same group and a larger id
have a larger group
Same with previous element: you need the last one from the list
have the same group and a smaller id
have a smaller group
Those fetch all next and previous entries respectively. If you need only one, use Rails' first and last (as suggested by Rails Guy) or limit(1) (and be wary of the asc/desc ordering).
This is what order_query does. Please try the latest version, I can help if it doesn't work for you:
class Product < ActiveRecord::Base
order_query :my_order,
[:product_sub_group_id, :asc],
[:id, :asc]
default_scope -> { my_order }
end
#product.my_order(#collection.products).next
#collection.products.my_order_at(#product).next
This runs one query loading only the next record. Read more on Github.

Selective ActiveRecord

Say I'm having a table 'Users'.
A user can exist 3 times (records) in my table, in 3 different states (state1, state2, state3).
First state1 will be created, then state2, ...
If state3 exists, I don't want to look at state1 and state2 anymore, but I'll have to keep them in my table, for later purposes.
All 3 records have the same uuid.
If I want to collect all users, I can't use User.all (because he will give me all 3 states for the same user).
Is there a short solution for this in my model? Now I'm collecting all uuid's and for each uuid I'll check which is the latest state, then I put those records in an array.
Problem with this array is that it is just 'an array', instead of an ActiveRecord object.
#uuid = []
#users = [] #will contain only the latest states at the end
User.all.each do |u|
#uuid << u.uuid unless #uuid.includes?(u.uuid)
end
#uuid.each do |u|
if user = User.find_by_state_and_uuid(3, u)
#users << user
elsif user = User.find_by_state_and_uuid(2, u)
#users << user
elsif user = User.find_by_state_and_uuid(1, u)
#users << user
end
end
Any ideas how I can translate this logic to an ActiveRecord object?
In short: User.magic_function to return only the latest state of each uuid
Thanks in advance!
Wouter
If you plan ahead, you can always sort on your state and return the "highest" one. This works well if you have a linear progression from one to the next. As an example:
user = User.where(:uuid => u).order('users.state DESC').first
For more complicated transitions you're not going to be able to use this trick. You could try using a different column for ordering, such as fetching the last by created_at time.
From a design perspective it seems highly unusual to have several user records in different states. A better plan might be to split out the state-driven part of the user record into a separate table and do the state tracking there, everything linked back to a singular user record.
Have you looked into using scopes? You should be able to create a scope for each User state, and then use those for querying.
Try :
User.get_user(state, uuid)
And make the scope in your user model :
scope :get_user, lambda { |*args| { :conditions => ["state = ? AND uuid = ?",args.first , args.second ] }}

Getting the value of an attribute based on its position/index inside #object?

I'm trying to figure out how to get the index value of an objects attribute. And after searching stack and the railsapi I'm still not sure how I should approach this
#groups = Group.where(:group_id => 1).first
Groups model would contain attribute :participant_id and has_many :participants
How would one be able to get a :participant_id based on its index?
For example how to get the First and Third :participant_id based on its position?
Something like:
= #group.participant_id(index position 1)
= #group.participant_id(index position 3)
Using SQL/ActiveRecord, you may use limit and offset methods applied to the "Group to Participants" relation:
# First
#group.participants.order(:id).offset(0).limit(1)
# Second
#group.participants.order(:id).offset(1).limit(1)
# First, second and third
#group.participants.order(:id).offset(0).limit(3)
Or, if you want to load all participants in a single SQL query :
#group.participants.order(:id)[index]
Or, a combination of both approaches.

ActiveRecord query returns an incorrect model

I have been scratching my head over this one for a little while, and though I'm sure its a stupid mistake, I've reached the point where I must consult SO if I am to preserve the hair follicles I have left.
I've written a function in Rails (3.1.2) which should return an array populated with ActiveRecord model objects (users, in this case) which meet a certain criterion. The criterion is that the user's current list (denoted by the field active_list_id) must not be nil. The code follows:
def build_list_array
#lists = Array.new
User.all.each do |user|
#active_list_id = user.active_list_id
#lists<< List.find(#active_list_id) if #active_list_id != nil #TODO WHAT?!? WHY IS THIS RETURNING USERS?
end
end
As you can see, I'm initializing an empty array, cycling through all users and adding their active list to the array if the relevant reference on the user record is not nil. The problem is, this is returning user objects, not list objects.
Here are the associations from the user and list models:
user model:
has_many :lists
has_many :tasks
list model:
belongs_to :user
A brief word about the reference to active_list: A user can have many lists, but only one is active at any time. Therefore, I need to reference that list on the user record. The active list is not a foreign key in the typical sense, then.
I appreciate any help you can give me...Thanks =)
As it stands, your build_list_array will return an array of User because of the behavior of each. When iterating over a collection using each, the call to each returns the original collection.
For example,
list = []
# returns => []
[1,2,3,4,5].each { |number| list << number * 10 }
# returns => [1, 2, 3, 4, 5]
list
# returns => [10, 20, 30, 40, 50]
In your code, the last statement in your build_list_array method is the each call, meaning the return value of each is what is returned by the method. If you simply added a return statement at the end of the method you would be good to go.
def build_list_array
#lists = Array.new
User.all.each do |user|
#active_list_id = user.active_list_id
#lists<< List.find(#active_list_id) if #active_list_id
end
return #lists # Actually return #lists
end
That being said, you should probably use something like Bradley's answer as a basis for more "correct" Rails code.
each always returns the collection it iterates on (no matter what happens inside the block). Sounds like you want to return #lists at the end of your method.
You seem to be making a curious use of instance variables. You could also fetch this in one query via a join, something along the lines of
List.joins('inner join users on active_list_id =lists.id')
Activerecord's Arel is your friend here:
User.where(:active_list_id.not_eq => nil)
Extending Steven's answer, to get the Lists
class User
belongs_to :active_list, :class_name => "List"
def build_list_array
#lists = User.where('active_list_id is not null').map(&:active_list).compact

How do I calculate the most popular combination of a order lines? (or any similar order/order lines db arrangement)

I'm using Ruby on Rails. I have a couple of models which fit the normal order/order lines arrangement, i.e.
class Order
has_many :order_lines
end
class OrderLines
belongs_to :order
belongs_to :product
end
class Product
has_many :order_lines
end
(greatly simplified from my real model!)
It's fairly straightforward to work out the most popular individual products via order line, but what magical ruby-fu could I use to calculate the most popular combination(s) of products ordered.
Cheers,
Graeme
My suggestion is to create an array a of Product.id numbers for each order and then do the equivalent of
h = Hash.new(0)
# for each a
h[a.sort.hash] += 1
You will naturally need to consider the scale of your operation and how much you are willing to approximate the results.
External Solution
Create a "Combination" model and index the table by the hash, then each order could increment a counter field. Another field would record exactly which combination that hash value referred to.
In-memory Solution
Look at the last 100 orders and recompute the order popularity in memory when you need it. Hash#sort will give you a sorted list of popularity hashes. You could either make a composite object that remembered what order combination was being counted, or just scan the original data looking for the hash value.
Thanks for the tip digitalross. I followed the external solution idea and did the following. It varies slightly from the suggestion as it keeps a record of individual order_combos, rather than storing a counter so it's possible to query by date as well e.g. most popular top 10 orders in the last week.
I created a method in my order which converts the list of order items to a comma separated string.
def to_s
order_lines.sort.map { |ol| ol.id }.join(",")
end
I then added a filter so the combo is created every time an order is placed.
after_save :create_order_combo
def create_order_combo
oc = OrderCombo.create(:user => user, :combo => self.to_s)
end
And finally my OrderCombo class looks something like below. I've also included a cached version of the method.
class OrderCombo
belongs_to :user
scope :by_user, lambda{ |user| where(:user_id => user.id) }
def self.top_n_orders_by_user(user,count=10)
OrderCombo.by_user(user).count(:group => :combo).sort { |a,b| a[1] <=> b[1] }.reverse[0..count-1]
end
def self.cached_top_orders_by_user(user,count=10)
Rails.cache.fetch("order_combo_#{user.id.to_s}_#{count.to_s}", :expiry => 10.minutes) { OrderCombo.top_n_orders_by_user(user, count) }
end
end
It's not perfect as it doesn't take into account increased popularity when someone orders more of one item in an order.

Resources