Ruby on Rails search 2 models - ruby-on-rails

Right... I've spent 3 days trying to do this myself to no a vale.
I have 2 models called Film and Screenings. Screenings belongs_to Film, Film has_many Screenings.
The Film has certain attributes(:title, :date_of_release, :description, :genre).
The Screening has the attributes(:start_time, :date_being_screened, :film_id(foreign key of Film)).
What I am trying to do is create a Search against both of these models.
I want to do something like this...
#films = Film.advanced_search(params[:genre], params[:title], params[:start_time], params[:date_showing])
And then in the Film model...
def self.advanced_search(genre, title, start_time, date)
search_string = "%" + title + "%"
self.find(:all, :conditions => ["title LIKE ? OR genre = ? OR start_time LIKE ? OR date_showing = ?", title, genre, start_time, date], order: 'title')
end
end
I don't think this could ever work quite like this, but I'm hoping my explanation is detailed enough for anyone to understand what im TRYING to do?? :-/
Thanks for any help guys

I would extract the search capability into a separate (non-ActiveRecord) class, such as AdvancedSearch as it doesn't neatly fit into either the Film or Screening class.
Rather than writing a complex SQL query, you could just search the films, then the screenings, and combine the results, for example:
class AdvancedSearch
def self.search
film_matches = Film.advanced_search(...) # return an Array of Film objects
screening_matches = Screening.advanced_search(...) # return an Array of Screening objects
# combine the results
results = film_matches + screening_matches.map(&:film)
results.uniq # may be necessary to remove duplicates
end
end
Update
Let's say your advanced search form has two fields - Genre and Location. So when you submit the form, the params sent are:
{ :genre => 'Comedy', :location => 'London' }
Your controller would then something like:
def advanced_search(params)
film_matches = Film.advanced_search(:genre => params[:genre])
screening_matches = Screening.advanced_search(:location => params[:location])
# remaining code as above
end
i.e. you're splitting the params, sending each to a different model to run a search, and then combining the results.
This is essentially an OR match - it would return films that match the genre or are being screened at that specified venue. (If you wanted and AND match you would need to the work out the array intersection).

I wanted to write something but this cast says all http://railscasts.com/episodes/111-advanced-search-form
Almost the same case as yours.

Related

Ruby/rails sort_by not ordering properly?

I have a long array of Photo model objects, and I want to sort them by created_at, newest first, then get a new array with the first 21 photos.
My problem is that the final array is not ordered properly.
Here is my code:
#recent_photos = photos.sort_by(&:created_at).reverse.first(21)
when I print out #recent_photos the created_at values are ordered like this:
1458948707
1458943713
1458947042
1458945171
...
What is the correct way to sort objects?
UPDATE:
here's how the initial list is compiled:
photos = #user.photos
#following = #user.following
#following.each do |f|
photos += f.photos if f.id != #user.id
end
#user.memberships.each do |group|
photos += group.photos
end
SOLUTION:
problem was with the question - I wanted to sort by timestamp not created_at, and those were timestamp values in the output
You can crunch it all down into a single query:
#recent_photos = Photo.where(
user_id: #user.following_ids
).order('created_at DESC').limit(21)
You really do not want to be doing N queries for each of these as it will get slower and slower as a person has more people they're following. If they follow 10,000 people that's a ridiculous number of queries.
If you add a :through definition to your model you may even be able to query the photos directly:
has_many :follower_photos,
class_name: 'Photo',
through: :followers
Whatever your constraints are, boil them down to something you can query in one shot whenever possible. If that's not practical, get it down to a predictable number of queries, never N.
Try:
#recent_photos = Photo.order('created_at desc').first(21)

How to sort by results of a method

I have a method on my User.rb model in my Rails app that returns a reputation score based on a user's various contributions to the site.
def total_votes
answerkarma = AnswerVote.joins(:answer).where(answers: {user_id: self.id}).sum('value')
contributionkarma = Contribution.where(user_id: self.id).sum('value')
answerkarma + contributionkarma
end
In the index action of my imaginary artists controller, sometimes I retrieve all artists, and sometimes I retrieve only those filtered by location
def index
if params[:province_id]
province = params[:province_id]
province = province.to_i
#artistsbyprovince = User.artists_by_province(province)
else
#artists = User.where(:onionpealer => true)
end
end
If I'm filtering by location, the User model calls this scope method
scope :artists_by_province, lambda {|province|
joins(:contact).
where( contacts: {province_id: province},
users: {onionpealer: true})
}
What I'd like to do, however, is, in both cases (whether filtering by location or retrieving all users) is to sort the retrieval of the artists by the results of total_votes method, so that artists with the highest number of points appear at the top.
Can you suggest how I might do that. Thank you in advance.
If do not pay attention on wrong query architecture, sorting can be done by ruby array#sort_by
#artistsbyprovince = User.artists_by_province(province).sort_by{ |u| -u.total_votes }

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.

Rails, custom finders

So I want to be able to get an object using find_by_id_or_name, I feel like I saw another question like this but am trouble finding any resources on making my own finder.
You can do this by adding a class method to your model e.g.
class Model < ActiveRecord::Base
def self.find_by_id_or_name(id_or_name)
find :first, :conditions => ['id = ? or name = ?', id_or_name, id_or_name]
end
def self.find_all_by_id_or_name(id_or_name)
find :all, :conditions => ['id = ? or name = ?', id_or_name, id_or_name]
end
end
You will then by able to do
Model.find_by_id_or_name(id_or_name)
You can customise the method slightly depending on your requirements e.g. if you want to try by id first and then by name you could use Model.exists? first to see if there is matching record before doing a find_by_name. You could also look if id_or_name consisted of characters 0-9 and assume that was an id and only search by name if it contained other characters.

How to filter results by multiple fields?

I am working on a survey application in ruby on rails and on the results page I want to let users filter the answers by a bunch of demographic questions I asked at the start of the survey.
For example I asked users what their gender and career was. So I was thinking of having dropdowns for gender and career. Both dropdowns would default to all but if a user selected female and marketer then my results page would so only answers from female marketers.
I think the right way of doing this is to use named_scopes where I have a named_scope for every one of my demographic questions, in this example gender and career, which would take in a sanitized value from the dropdown to use at the conditional but i'm unsure on how to dynamically create the named_scope chain since I have like 5 demographic questions and presumably some of them are going to be set to all.
You can chain named scopes together:
def index
#results = Results.scoped
#results = #results.gender(params[:gender]) unless params[:gender].blank?
#results = #results.career(params[:career]) unless params[:career].blank?
end
I prefer however to use the has_scope gem:
has_scope :gender
has_scope :career
def index
#results = apply_scopes(Results).all
end
If you use has_scope with inherited_resources, you don't even need to define the index action.
named_scope :gender,lambda { |*args|
unless args.first.blank?
{ :conditions => [ "gender = ?", args.first] }
end
}
If you write named scopes in this way, you can have all them chained, and if one of your params will be blank wont breaks.
Result.gender("Male") will return male results.
Result.gender("") will return male and female too.
And you can chain all of your methods like this. Finally as a filtering you can have like:
Result.age(16).gender("male").career("beginer")
Result.age(nil).gender("").career("advanced") - will return results with advanced career, etc.
Try some like this:
VistaFact.where( if active then {:field => :vista2} else {} end)
Or like this:
VistaFact.where(!data.blank? ? {:field=>data.strip} : {}).where(some? ? {:field2 => :data2} : {}).where ...
That work for me very nice!

Resources