I have a Location model that has_many LocationPhotos. The Location also belongs_to a single LocationPhoto as the featured_location_photo. The belongs_to association looks like this:
class Location < ActiveRecord::Base
belongs_to :featured_location_photo, class_name: 'LocationPhoto', foreign_key: 'featured_location_photo_id'
end
Using will_paginate, I can paginate the photos for a location like this:
location.location_photos.paginate(page: 1, per_page: 4)
Ideally, I would like to always include the featured_location_photo as the first element, on the first page, of the collection that's returned, and not be included again. So, for example, if the related photos have ids 1,2,3,4,5,6 and the featured photo has id 3, I would want page one to show photos with ids:
3, 1, 2, 4
And page two to show photos with ids:
5, 6
The collection should also show total_entries equal to 6.
Is there any way to do this? I can't think of a way to do it by scoping the query because there's no way to tell the featured photo to sort to the top of the results.
My other thought is to first check if the location has a featured photo. If it does, remove that from the query with a scope:
result = location.location_photos.where.not(id: [featured_photo_id]).paginate(page: 1, per_page: 4)
Then, I would need to add the featured photo back into the front of result.
I'm totally saving this, because I've needed to solve this problem before and always copped out on the solution. Thanks for making me find this!!!
To promote a specific record in a collection use:
order("case when id = #{id} then 0 else id end")
Which means you can just create a custom scope like so:
class Location < ActiveRecord::Base
def photos
location_photos.
order("case when id = #{featured_location_photo_id} then 0 else id end")
end
end
EDIT: actually, ActiveRecord relations respond to unshift just like arrays. So all you actually need is this:
def photos
location_photos.unshift( featured_location_photo )
end
Which of course you can then paginate.
I think that you could improve your models by removing the dual association and adding a column on the location photo model named featured.
This way you can have more than one featured photo for each location (eventually in the future this may be needed).
After that you can use:
location.location_photos.order(:featured).paginate(page: 1, per_page: 4)
And that would do the trick.
NOTE: I am aware that adding a column would cause most of the images to have an empty field on the table, but space is not a problem nowadays.
Related
I need to get all children from a parent as an ActiveRecord::Relation. Thing is, this children are stored in a polymorphic relation. In my case I need it to paginate some search results obtained with pg_search gem.
I've tried the following:
results = PgSearch.multisearch('query').map(&:searchable)
# Horrible solution, N + 1 and returns an array
docs = PgSearch.multisearch('query').includes(:searchable)
results = docs.map(&:searchable)
# Still getting an array
Also thought of things like select or pluck, but they are not intended for retrieving objects, only column data. I could try to search ids for each children type like so
Post.where(id: PgSearch.multisearch('query').where(searchable_type: "Post").select(:searchable_id)
Profile.where(id: PgSearch.multisearch('query').where(searchable_type: "Profile").select(:searchable_id)
But it doesn't scale, since I would need to do this for every object I want to obtain from a search result.
Any help would be appreciated.
EDIT
Here's some basic pseudocode demonstrating the issue:
class Profile < ApplicationRecord
has_one :search_document, :as => :searchable
end
class Post < ApplicationRecord
has_one :search_document, :as => :searchable
end
class Profile < ApplicationRecord
has_one :search_document, :as => :searchable
end
class SearchDocument < ApplicationRecord
belongs_to :searchable, plymorphic: true
end
I want to obtain all the searchable items as an ActiveRecord::Relation, so that I can dynamically filter them, in this specific case, using limit(x).offset(y)
SearchDocument.all.joins(:searchable).limit(10).offset(10)
Generates an error: cannot eagerly load searchable cause of polymorphic relation
SearchDocument.all.includes(:searchable).limit(10).offset(10)
This one does load the searchable items into memory, but does not return them in the query, instead it applies the filters to the SearchDocument items, as expected. This might be a temporary solution, to filter the search documents and then get the searchable items from them, but collides with pagination on the views.
The question here is: Is there a way I can get all searchable items as ActiveRecord::Relation to further filter them?
I'm unfamiliar with this library. However, looking at your code, I'd guess that any attempt to take a CollectionProxy and map some function over it will trigger evaluating the CollectionProxy and return an array.
Having had a quick look at the library GitHub docs, perhaps something like this might work:
post_docs = PgSearch.multisearch('query').where(searchable_type: "Post")
posts = Post.where(pg_search_document: post_docs)
I have a Track table and a Section table. A track has many sections. Sections are connected to their respective task by the Section's :track_id, which corresponds to the Track's :id attribute.
<% #track = Track.find(params[:id]) %>
<% #sections = Section.find_by_track_id(#track.id) %>
In the code above I'm trying to find multiple sections that share the same :track_id attribute, but find_by_track_id() only returns the first. What's the best way to get all of them?
Thanks!
If your tracks and sections are related in this way, then the best way to relate them is by using the methods that come automatically from Rails' associations.
in this case, I expect in your model files, you have the following:
class Track < ActiveRecord::Base
has_many :sections
end
class Section < ActiveRecord::Base
belongs_to :track
end
Then you can get the sections for a track like this:
#track = Track.find(params[:id])
#sections = #track.sections
You're looking for where, which finds all records where a specific set of conditions are met.
#sections = Section.where(track_id: #track.id)
This is unrelated to your question, but you should set #sections and #track in your controller. As it seems like you're new to Rails, I'd highly recommend reading through the Rails Guides. They will help you immensely on your journey.
EDIT: I was solving for the general question of "Find multiple database objects by attribute in Rails?", which is how to find multiple database objects in the general case. #TarynEast's method is the way to go to find all of the sections for a track, or more generally, all of the objects that belong to the desired object. For the specific case you're asking for above, go with #TarynEast's solution.
Association
To extend Taryn East's answer, you need to look into ActiveRecord Associations.
In your model, if you have the following has_many relationship:
#app/models/track.rb
Class Track < ActiveRecord::Base
has_many :sections
end
#app/models/section.rb
Class Section < ActiveRecord::Base
belongs_to :track
end
This will set up a relational database association between your tracks and sections datatables.
--
Associative Data
The magic of Rails comes into play here
When you call the "parent" object, you'll be able to locate it using its primary key (typically the ID). The magic happens when Rails automatically uses this primary_key as a foreign_key of the child model - allowing you to call all its data as an append to the parent object:
#track = Track.find params[:id] #-> find single Track by primary key
#sections = #track.sections #-> automagically finds sections using the track primary key
This means if you call the following, it will work exactly how you want:
#sections.each do |section|
section.name
end
Where
Finally, if you wanted to look up more than one record at a time, you should identify which ActiveRecord method you should use:
find is to locate a single record by id
finy_by key: "value" is to locate a single record by your defined key/column
where is to return multiple items using your own conditions
So to answer your base line question, you'll want to use where:
#sections = Section.where track_id: params[:id]
This is not the right answer, but it should help you
<% #sections=#track.sections%>
Use find when you are looking for one specific element identified by it's id.
Model.find is using the primary key column. Therefore there is always exactly one or no result.
A Miniatures model has many Collections. Users can have and vote for the best Collection version of a Miniature. The votes are in a model called Imagevotes which update a counter_cache attribute in the Collections model.
What I want to do is flag Collections which are ranked first for a Miniature as GOLD, then rank the 2nd, 3rd and 4th as SILVER. I realise I can do this on the Miniature model by selecting the #miniature.collection.first, but I would like to be able to store that like you would store the vote-count in a counter_cache so that I could display the total number of GOLDS or SILVERS for any one user.
Is there a way that each model could have Boolean fields called GOLD and SILVER which would be updated as new votes are cast in the same way that a counter_cache is updated?
Any pointers and further reading much appreciated.
Update:
It occurs to me that this could also be done with a sort of second index column. A vote_position column if you will, that updated with a number from "1" for the record with the highest counter_cache number and ascended from there. Then I could use #miniature.collection.where(:vote_position => "1") or similar. Perhaps this is more ideal?
As it seems for me you just need to implement method in Miniature model:
def set_gold_and_silver
top_collections = self.collections.order("imagevotes_count desc").limit(4)
gold = top_collections.shift
gold.update_attribute :is_gold, true if gold
top_collections.each {|s| s.update_attribute :is_silver, true}
end
after that you can add it to after_create filter of Imagevotes model:
class Imagevotes < ActiveRecord::Base
after_create :set_gold_and_silver
def set_gold_and_silver
self.miniature.set_gold_and_silver
end
end
Let's say I have a table posts, and another table reviews that has a post_id and a rating (integer).
How can I add a filter to app/admin/post.rb which returns posts with a certain total score? (e.g. SUM(reviews.rating) GROUP BY (posts.id)). I want the filter to show up on the right side of the index, along with the other filters, and ideally to function as a range input.
To be clear, when I say "filter", I mean the ActiveAdmin filter method which adds filters to the right sidebar on the index page.
I created a scope in Post which returns posts with scores, but I haven't been able to find a way to use that in an ActiveAdmin filter.
Note: I rewrote my example because my original one didn't capture the complexity of the question.
It's common to override scoped_collection to join associated records to increase performance:
ActiveAdmin.register Post do
controller do
def scoped_collection
super.includes :author, :publisher
end
end
end
Since the entire collection now has author and publisher included, you can have a scope that queries those:
scope :random_house do |scope|
scope.where publishers: {name: 'Random House'}
end
I haven't come up with a proper solution to this question, but I have found a workaround.
I can change the scoped_collection based on a query param, and simply pass the param in when I want to use it. For example, if I have a scope with_rating(x), which returns posts with a score of at least x, I can write:
controller do
def scoped_collection
if params[:with_rating]
super.with_rating(params[:with_rating])
else
super
end
end
end
Then I can go to /admin/posts?with_rating=100 and get back posts with a rating of at least 100.
Thanks to #seanlinsley for making me aware of the scoped_collection method that I used in this solution.
Use counter cache column to store the comments count
http://railscasts.com/episodes/23-counter-cache-column
Then the column will get updated each time a comment is created to that post.This would also help in increasing the performance of search.
I have a situation where the behavior of an existing app is changing and it's causing me a major headache.
My app has Photos. Photos have a status: "batch", "queue", or "complete". All the existing Photos in the app are "complete".
99% of the time I need to only show complete photos, and in all of the existing codebase I need every call to Photos to be limited to only complete photos.
However, in the screens related to uploading and classifying photos I need to be able to fairly easily override that default scope to show batched or queued photos.
Like many others, I need to find a way to easily override the default scope in certain situations. I looked at these questions (1, 2) and they don't seem to answer what I'm looking for.
The code I wish worked is this:
class Photo < ActiveRecord::Base
...
default_scope where(:status=>'complete')
scope :batch, unscoped.where(:status=>'batch')
scope :queue, unscoped.where(:status=>'queue')
...
end
However, that doesn't work. I tried wrapping the scope methods in lambdas, and that didn't work either.
I realize default_scope comes with baggage, but if I can't use it with overrides then I'm looking at adding scope :complete ... and having to comb through every call to photos in my existing app and add .complete in order to filter unprocessed photos.
How would you solve this problem?
def self.batch
Photo.unscoped.where(:status=>"batch")
end
This thread is more authoritative: Overriding a Rails default_scope
I give it a shot. Lets say you want to remove a where clause from a default scope (and not just override it with another value) and keep associations you can try this:
class Photo < ActiveRecord::Base
default_scope where(:status => 'complete').where(:deleted_at => '').order('id desc')
def self.without_default_status
# Get the ActiveRecord::Relation with the default_scope applied.
photos = scoped.with_default_scope
# Find the where clause that matches the where clause we want to remove
# from the default scope and delete it.
photos.where_values.delete_if { |query| query.to_sql == "\"photos\".\"status\" = 'complete'" }
photos
end
end