How to add a non-existant column to a sql query - ruby-on-rails

I have a little problem. I'm looking for a way to "add" a column to a sql query.
Actually I sort some product from a table with this order:
#prod = Table.find(:all,{:select=>["id, prod-id, cost"],:conditions=>["something = ? and somethingelse = ?","that thing","that other thing"],:order=>"id DESC"})
Then I put the result of this query into a Table (html) with a form to add the product into a Cart.
The problem comes now. In my Cart the product are like that:
cart: #<Cart:0x00000005ce0a68 #items=[[#<Table id: 6024, prod-id: 907509, cost: 33>]]>
And I need to add a "column" to the data in my cart. To get my Cart like that:
cart: #<Cart:0x00000005ce0a68 #items=[[#<Table id: 6024, prod-id: 907509, cost: 33, quantity: 1>]]>
Here is my Cart:
class Cart
attr_reader :items
def initialize
#items = []
#total_price = 0
end
def add_product(product)
#items << product
product
end
def total_price
#items.sum{ |item| item.prod_pxuttc}
end
def total_items
#items.length
end
end
I need to not-add any Column to my Table. Because I will have to import data from another Source Database into my Table, and I can't change the Source Database. (Yes it's a stupid process, but that's what my Customer want)
I have totaly no idea about how to give name to the process I need to do.
[edit]
I found a way to resolve my problem.
By creating a second table, with the same column plus column quantity, with a default empty row.
Then I select my product from First Table, the default row from the Second Table, and I put value from product into the variable of the default row. Then I put my new variable into my Cart.
It's a bit tricky but it works.

Maybe define Cart as the following will solve the problem.
class Product
attr_accessor quantity
end
class Cart
...
end

Related

Rails Model Next/Prev Navigation Scope with datetime field

I have a model lets say Post which has a column called published_at (datetime). My model has two instance methods called next and prev.
I'm using mysql as a database adapter.
def next
self.class.unscope(:order).online
.where('published_at > ?', published_at)
.order('published_at ASC').first
end
def prev
self.class.unscope(:order).online
.where('published_at < ?', published_at)
.order('published_at DESC').first
end
With the code above the navigation works if every post has another date. If some posts are on the same date just one record of those gets shown.
Goal: Get the next or previous record ordered by published_at datetime column.
Any ideas?
Neither "next" , nor "previous" make sense in SQL. You have to explicitly define sort order by uniq key.
If you have non-uniq column as sort base, you have to add uniq one, id for example.
Then you have to change WHERE clause to accept rows wich have same value for your non-uniq column:
def next
condition = <<~SQL
(published_at = :published_at AND id > :id) OR
(published_at > :published_at)
SQL
self.class.unscope(:order).
online.
where(condition, published_at: published_at, id: id).
order(published_at: :asc, id: :asc).
take
end
prev is constructed similarly.

How to sort the ActiveAdmin index based on a filter and a column

I have a Rails application with a Movie model. The Movie model has 'name' and 'release_date' as regular attributes, as well as a scope used to search for movie names with elasticsearch.
class Movie < ActiveRecord::Base
scope :movie_name_search, -> (term) {
movie_ids = elasticSearch(term, :name).map(&id)
Movie.where(id: movie_ids).reorder('').order_by_ids(movie_ids) unless movie_ids.nil?
}
end
I then set up my active admin to show this data
ActiveAdmin.register Promotion do
filter :movie_name_search, as: :string, label: "Movie Name"
index do
actions
column :name
column :release date, sortable: :release_date
end
end
Putting in a movie name into the search bar works perfectly, and sorting against release_date works perfectly, but I can't do both at the same time. Once I'm using the filter for movie names, the sort by date doesn't work. It only works when I remove the reorder and new order.
scope :movie_name_search, -> (term) {
movie_ids = elasticSearch(term, :name).map(&id)
Movie.where(id: movie_ids) unless movie_ids.nil?
}
It would appear that the ordering I enforce in the scope takes precedence over the sort of the column but I have no idea why. Any ideas?
You're resetting the scope chain when you call Movie.where in movie_search_name. You want to send where to self instead (i.e. just delete the Movie. part), so that prior conditions are preserved.
scope :movie_name_search, -> (term) {
movie_ids = elasticSearch(term, :name).map(&id)
where(id: movie_ids) unless movie_ids.nil?
}
Edit: I understand the issue now
Like you say, Elastic Search is returning an array of ids in sorted order, but where does not respect that order. It just pulls records from the database as it finds them, so long as their ids are in the array. It's no good for us to sort the records afterwards as an array, because then ActiveRecord can't apply additional clauses to the query. It has to be done as part of the query.
SQL does provide a way to enforce an arbitrary order: ORDER BY CASE, but it's not built in to Rails. Fortunately, it's not hard to add.
Let's suppose your search results are [2, 1, 3]. In SQL, we can retrieve them in order like this:
SELECT * FROM movies
WHERE id IN (2, 1, 3)
ORDER BY CASE id
WHEN 2 THEN 0
WHEN 1 THEN 1
WHEN 3 THEN 2
ELSE 3 END;
To make this compatible with ActiveRecord, we can add a class method to Movie:
app/models/movie.rb
class Movie < ActiveRecord::Base
# ...
def self.order_by_ids_array(ids)
order_clause = "CASE id "
ids.each_with_index do |id, index|
order_clause << sanitize_sql_array(["WHEN ? THEN ? ", id, index])
end
order_clause << sanitize_sql_array(["ELSE ? END", ids.length])
order(order_clause)
end
end
Now your ActiveAdmin scope uses both where and order_by_ids_array:
scope :movie_name_search, -> (term) {
movie_ids = elasticSearch(term, :name).map(&id)
where(id: movie_ids).order_by_ids_array(movie_ids) unless movie_ids.nil?
}
Reference:
http://www.justinweiss.com/articles/how-to-select-database-records-in-an-arbitrary-order/
Edit II: A real hack
Note: This requires a recent version of ActiveAdmin that uses Ransack.
The issue we're having is that filters don't play well with sorting. So here's
the new plan: let's add another column to our index table that shows the search
rank of each movie. This column will only appear when we've filtered by movie
name, and it will be sortable. This way there will be no "default" sort, but
you can sort by anything you want, including search ranking.
The trick is to insert a computed column into the query using a CASE like
above, but in the SELECT clause. We'll call it search_rank and it can be
accessed on any returned movie as movie.attributes['search_rank'].
app/admin/movies.rb
ActiveAdmin.register Movie do
filter :movie_search, as: string, label: 'Movie Name'
index do
# only show this column when a search term is present
if params[:q] && params[:q][:movie_search]
# we'll alias this column to `search_rank` in our scope so it can be sorted by
column :search_rank, sortable: 'search_rank' do |movie|
movie.attributes['search_rank']
end
end
end
end
Now in our model, we need to define the movie_search scope (as a class method)
and mark it as ransackable.
app/models/movie.rb
class Movie < ActiveRecord::Base
def self.ransackable_scopes(opts)
[:movie_search]
end
def self.movie_search(term)
# do search here
ids = elasticSearch(term, :name).map(&id)
# build extra column
rank_col = "(CASE movies.id "
ids.each_with_index do |id, index|
rank_col << "WHEN #{ id } THEN #{ index } "
end
rank_col << 'ELSE NULL END) AS search_rank'
select("movies.*, #{ rank_col }").where(id: ids)
end
end

active_admin config.sort for two columns

I have an active_admin table called shows that acts of a list of rsvps for bike riders and bike shows that the riders will compete in. The following code correctly sorts the table alphabetically by rider_last_name:
config.sort_order = 'rider_last_name_asc'
Now when a rider is attending multiple bike shows, I want the table to first sort by rider_last_name and then within that rider sort by an attribute of shows called start_time. start_time is a DateTime. According to this stackoverflow article, the following should work:
config.sort_order = 'rider_last_name_asc, start_time_asc'
but it doesn't. In fact, it undoes the sorting by rider_last_name. How do I sort by both columns?
You may try refine apply_sorting method for collections, like this:
controller do
def apply_sorting(chain)
params[:order] ? chain : chain.reorder(rider_last_name: :asc, start_time: :asc)
end
end
ActiveAdmin suggests overwrite the find_collection method, which returns an ActiveRecord::Relation. But I prefer add the order to it's output.
ActiveAdmin.register Show do
controller do
def find_collection(options = {})
super.reorder(rider_last_name: :asc, start_time: :asc)
end
end
...
end
Although it works, this overwrite the user option of click on a column.
Alternative
You can do the same with scoped_collection, which is called at the start of find_collection, but it does not work unless you add active_admin_config.sort_order = '' to the controller. This way:
controller do
active_admin_config.sort_order = ''
def scoped_collection
super.reorder(rider_last_name: :asc, start_time: :asc)
end
end
Now, if we want to reorder before and, after that take care of the user params (and do not overwrite them). This is the way.
Note: I did use active_admin_config.sort_order and not config.sort_order.
Also, the sort_order option, ends as a param of OrderClause.new call, which expect for only one sort field, see here and here.
you can try to pass an array to it as such:
config.sort_order = [:rider_last_name_asc, :start_time_asc]

Eager Loading Optimization in ruby on rails

I have a product that has_many Variants. And a variant belongs to a product. I want to display the name of the product(which can be found in Product) and the price and quantity(which can be found in Variants).
Product table:
-id
-title
-description
Variants table:
- id
- is_active(boolean)
- price
- quantity
- product_id
This is how the table looks like. And this is my attempt
def index
#products =Product.all
#display = []
#products.each do |product|
#children = product.variants.where(is_active: true).order(:price).
select(:id,:price,:quantity).first
if #children.present?
#display << {
id: product.id,
title: product.title,
description: product.description,
price: #children.price,
quantity: #children.quantity,
variant_id: #children.id
}
end
end
#display = Kaminari.paginate_array(#display).page(params[:page])
end
I need to optimize this to the maximum. Which brings me to my first question. How can I optimize this better .
And my 2nd question why when I do #products = Product.all.includes(:variants) it will actually increase the loading time instead of lowering it since I do get the variants for every product in that iteration over the whole #products array(so it should count as an N+1, I get products for each product I get variants)?
Is spliting my array of #products in 4 and making 4 threads populate display a good idea?
Your code isn't using the eager loaded data which is why adding includes is slowing things down - it's just extra work.
In general if you call query methods ( where, order, etc) rails can't use the eager loaded data. Instead you can make a new association:
has_many :active_variants, -> { where(is_active: true).order(:price) }, class_name: "Variant"
Then eager load and use this association instead of the variants association.
You should write it as;
def index
#display = Varient.joins(:product).where(is_active: true).order(:price).select('products.id as id, products.title, products.description, price, quantity, variants.id as variant_id')
end

RoR sum related records during controller create

I have tables = expenses and invlines (invoice lines).
Expense belongs_to Invline
Invline has_many expenses
Using the Invlines input form, I have the user select the Expenses.
When the Invlines record is saved, I would like to summarize the expenses and place into invline.price
In the Invlines controller I'm trying to use:
def create
#invline = Invline.new(params[:invline])
#invline.price ||= #invline.expenses.amount.sum
But, I get
undefined method `amount'
Ideas?
Thanks!
Replace:
#invline.price ||= #invline.expenses.amount.sum
with
#invline.price ||= #invline.expenses.sum(:amount)
The .expenses call returns a list of expenses, but only a particular expense has amount.

Resources