I have a model for which I want to retrieve the next record(s) and previous record(s). I want to do this via a named_scope on the model, and also pass in as an argument the X number of next/previous records to return.
For example, let's say I have 5 records:
Record1
Record2
Record3
Record4
Record5
I want to be able to call Model.previous or Model.previous(1) to return Record2. Similarly, I want to be able to call Model.next or Model.next(1) to return Record4. As another example I want to be able to call Model.previous(2) to return Record3. I think you get the idea.
How can I accomplish this?
To implement something like 'Model.previous', the class itself would have to have a 'current' state. That would make sense if the 'current' record (perhaps in a publishing scheduling system?) was Record3 in your example, but your example doesn't suggest this.
If you want to take an instance of a model and get the next or previous record, the following is a simple example:
class Page < ActiveRecord::Base
def previous(offset = 0)
self.class.first(:conditions => ['id < ?', self.id], :limit => 1, :offset => offset, :order => "id DESC")
end
def next(offset = 0)
self.class.first(:conditions => ['id > ?', self.id], :limit => 1, :offset => offset, :order => "id ASC")
end
end
If so you could do something like:
#page = Page.find(4)
#page.previous
Also working would be:
#page.previous(1)
#page.next
#page.next(1)
Obviously, this assumes that the idea of 'next' and 'previous' is by the 'id' field, which probably wouldn't extend very well over the life of an application.
If you did want to use this on the class, you could perhaps extend this into a named scope that takes the 'current' record as an argument. Something like this:
named_scope :previous, lambda { |current, offset| { :conditions => ['id < ?', current], :limit => 1, :offset => offset, :order => "id DESC" }}
Which means you could call:
Page.previous(4,1)
Where '4' is the id of the record you want to start on, and 1 is the number you want to navigate back.
I added this feature into my gem by_star last month. It may have some other features you're interested in, so check it out.
It works by you having an existing record and then calling previous or next on that:
Page.find(:last).previous
I wrote a gem for doing this for arbitrary order conditions, order_query:
class Issue < ActiveRecord::Base
include OrderQuery
order_query :order_display, [
[:priority, %w(high medium low)],
[:valid_votes_count, :desc, sql: '(votes - suspicious_votes)'],
[:updated_at, :desc],
[:id, :desc]
]
def valid_votes_count
votes - suspicious_votes
end
end
Issue.order_display #=> ActiveRecord::Relation<...>
Issue.reverse_order_display #=> ActiveRecord::Relation<...>
# get the order object, scope default: Post.all
p = Issue.find(31).order_list(scope) #=> OrderQuery::RelativeOrder<...>
p.before #=> ActiveRecord::Relation<...>
p.previous #=> Issue<...>
p.position #=> 5
p.next #=> Issue<...>
p.after #=> ActiveRecord::Relation<...>
Related
I'm trying to look up 1 instance of a model via a slug, which I can do via
#project = Project.where(:slug => params[:id]).first
But once I get the instance I want to find the next and previous instances of the model from a order of a column called publish which is a date.
how would I get the slug only of the previous and next instance of my arbitrary order by column?
::Edit::
Adding this to my modeled worked.
def previous(offset = 0)
self.class.select('slug').first(:conditions => ['published < ?', self.id], :limit => 1, :offset => offset, :order => "published DESC")
end
def next(offset = 0)
self.class.select('slug').first(:conditions => ['published > ?', self.id], :limit => 1, :offset => offset, :order => "published ASC")
end
I found this solution at Next, Previous Records Using Named Scope
Assuming your publish dates are unique something like this ought to work:
#project = Project.where(:slug => params[:id]).first
#prev_project = Project.where( "publish < ?", #project.publish ).
order( "publish DESC" ).
first
#next_project = Project.where( "publish > ?", #project.publish ).
order( "publish" ).
first
For #prev_project the where clause indicates all Projects before #project, the order lists them latest first (which puts the one right before #project at the top) and first limits the query to one result, i.e. the previous Project. The query for #next_project is the same but reversed. If publish isn't unique you'll have to sort on a second criterium as well.
However, you may want to consider not reinventing the wheel and using the very common gem acts_as_list, which would make this as simple as #project.higher_item and #project.lower_item.
I'm trying to do something along the following lines:
if start_date?
query = where("datetime > :start_date", :start_date => start_date)
end
if end_date?
query = where("datetime <= :end_date", :end_date => end_date)
end
if client
query = where("client.name LIKE :client", :client => client)
end
if staff
query = where("staff.name LIKE :staff", :staff => staff)
end
paginate :per_page => 20, :page => page,
:conditions => query,
:order => 'datetime'
I can't find any examples though of how to build up a query and use it along with paginate plugin. Of course the query isn't correct yet as I've got to figure out how to do the joins so I can query against the other properties in other tables, but other than that, how do you build up queries to use in the conditions?
You want to put the .paginate call on the end of the chain of where calls, like this (all one line, split up for readability)
SomeClass.where("datetime > :start_date", :start_date=>start_date).
where("datetime < :end_date", :end_date=>end_date).
paginate(:per_page=>20,:page=>page)
If you want to conditionally add the conditions, you can do that too, like:
x = SomeClass
x = x.where("datetime > :start_date", :start_date=>start_date) if start_date
x = x.where("datetime < :end_date", :end_date=>end_date) if end_date
x.paginate(:per_page=>20,:page=>page)
You can try this if you installed kaminari gem :
ModelName.where( your_query ).page(params[:page]).per(50)
.where() returns ActiveRecord::Relation
.paginate()'s :conditions parameter has to be an array of conditions as if it was an argument of .where().
You should fix your code as next:
if start_date?
query = ["datetime > :start_date", :start_date => start_date]
end
etc.
ActiveRecord Relation doesn't have paginate method
I am working on a Rails 2.3.9 app and my question involves both a self referencial relationship and a named_scope. This application allows users to log and share workouts. A workout can be public or private and that is designated by #workout.public == 1.
I allow users to 'follow' people. So on a current_user's dashboard I display all public workouts from users that current_user follows with the following code:
/dashboard/index.html.erb
<% current_user.friends_workouts.each do |workout| %>
<%= link_to (workout.title), workout %> <br/>
by <%= link_to (workout.user.username), workout.user %> - <%= time_ago_in_words(workout.created_at)%> ago</p>
<% end %>
user.rb
def friends_workouts
#friends_workouts ||= Workout.current.public_workouts.find_all_by_user_id(self.friends.map(&:id), :order => "created_at DESC", :limit => 3)
end
workout.rb
named_scope :public_workouts, :conditions => {:public => 1 }, :order => "created_at DESC"
I now want to add a condition to this scope as I am adding another level of sharing. Users can associate to a "box" (a gym really) through a "membership" model. So if the current_user belongs_to the same "box" as a user they follow, they should not only see the workouts marked public but also workouts where #workout.box_only == 1.
How can I affect the above to include all public workouts from followed users AND workouts from followed users where #workout.box_only == 1 and #workout.user.membership.box_id == current_user.membership.box_id. I know that syntax is incorrect but you get my point (hopefully).
UPDATE:
It also needs to be considered that :public_workouts is being called from pages that don't require a logged_in? user so in that case if the scope is trying to reference current_user it will throw an error.
UPDATE 2:
:user has_many :memberships
I believe something like the following should do it for you:
named_scope :public_workouts,
:joins => ", user, membership"
:conditions =>
"workouts.public = 1 or
membership.box_id = #{current_user.membership.box_id}",
:group => "workouts.id",
:order => "workouts.created_at DESC"
You would have to play around with this for a bit. The hard part every time I try something like this is to get the OR conditions correct. You want to get all public and those where the joined membership.box_id matches regardless of public being 1.
Edit: Admittedly this is perhaps not the most ruby way of building a query and I haven't tested it properly but something like below could also be made to work.
def self.public_workouts
query = Workout.joins(:user => { :membership })
if current_user
query.where('memberships.box_id = ? or workouts.public = 1', current_user.membership.box_id) unless current_user.membership.box_id.nil?
else
query.where('workouts.public = 1')
end
query.group('workouts.id')
query.order("workouts.created_at DESC")
return query
end
Edit2
Another alternative could be to create two separate scopes and create a class method that combines the two scopes. This class method would then be used in the view.
named_scope :box_workouts,
:joins => ", user, membership"
:conditions => "memberships.box_id = #{current_user.membership.box_id}"
:group => "workouts.id",
:order => "workouts.created_at DESC",
:select "workouts"
named_scope :public_workouts,
:conditions => :public => 1
:order => "workouts.created_at DESC"
def self.public_box_workouts
return box_workouts.merge(public_workouts).limit(3) if current_user
return public_workouts.limit(3)
end
Edit3 Not so hard, I believe something like below will work.
def self.box_and_public_workouts(user)
return public_workouts if user.nil? or user.memberships.blank?
return public_workouts + box_workouts(user.memberships.map(&:box_id))
end
named_scope :box_workouts, lambda { |box_ids| { :conditions => ['box_id IN (?)', box_ids], :order => 'created_at DESC' } }
Apologies for taking so long. I was missing confused with how the "old" way of querying the database. I went right for Rails3 :)
Anyway, I didn't want to commit anything so I tried to fork it to send a pull request but github is being rude to me tonight. Might just copy from here then.
I have a model that has ratings in it for an post.
I want to display a list of the top 5 ratings for a given post, but I am completely lost on where to start. I figure the find method might have something useful, but I'm unsure. I've even considered looping through each record getting its size, adding it to a hash, sorting the hash, etc., but that seems too complicated.
Does anyone know how I might accomplish something like this?
Thank you
Edit: I found this to get all the posts that have the rating of agree:
Post.find(:all, :include => :post_ratings, :condtions => ['post_ratings.agree = ?', true])
The only problem is I can't figure out how to get the top five ratings from this query.
Might be worth giving a little more of an example of the code you're working with but I'll answer with a few assumptions.
If you have:
class Post
has_many :post_ratings
end
class PostRating
belongs_to :post
# Has a 'rating' attribute
end
You can find the top five post ratings with:
p = Post.find(:first) # For example
p.post_ratings.find(:all, :limit => 5, :order => 'rating desc')
To get the top five post ratings overall you can do:
PostRating.find(:all, :limit => 5, :order => 'rating desc')
UPDATE:
Following your edit it seems you have an 'agree' and a 'disagree' column. Not sure how that works in combination so I'll stick with the 'agree' column. What you'll need to do is count the ratings with agree flagged. Something like:
count_hsh PostRating.count(:group => 'post_id',
:order => 'count(*) desc',
:conditions => { :agree => true },
:limit => 5)
This will return you a hash mapping the post id to the count of agree ratings. You can then use that post_id to locate the posts themselves. The ratings are provided by the counts so the individual ratings are (I think) of no use though you could access them by calling post.post_ratings.
So, to get the top five posts:
#top_five_posts = []
count_hsh.each_pair do |post_id, ratings|
p = Post.find(post_id)
p[:rating_count] = ratings
#top_five_posts << p
end
This is probably more verbose than it could be but is hopefully illustrative. The p[:rating_count] is a virtual attribute which isn't in the database but will allow you to access the .rating_count method on the posts in your view if you wish to.
Assuming the same Post and PostRating from Shadwell's answer:
class Post
has_many :post_ratings
end
class PostRating
belongs_to :post
# Has a 'rating' attribute
end
To get the top five ratings for all Posts:
post_ratings.find(:all, :limit => 5, :order => 'rating desc')
To get the top five ratings for a specific Post you can:
p = Post.find(:first)
post_ratings.find_all_by_post_id(p.id, :limit => 5, :order => 'rating desc')
To find all posts sorted by average rating, you can use ActiveRecord::Calculations.
PostRating.average(:rating, :group => :post_id, :include => :post, :order => 'average desc')
For Example, I want to know the User have many posts. So, I can get back post using this :
#user.posts
but I don't want to get all the posts back. I would like to limite the result, for example, top ten created, or may be sorted by some column. How can I do so? Thank you.
You can always make a generic scope to handle the limit, such as putting this in an initializer:
class ActiveRecord::Base
named_scope :limit, lambda { |*limit| {
:limit => limit[0] || 10,
:offset => limit[1]
}}
end
This makes limiting queries easy:
# Default is limited to 10
#user.posts.limit
# Pass in a specific limit
#user.posts.limit(25)
# Pass in a specific limit and offset
#user.posts.limit(25, 25)
For something more robust, you might want to investigate will_paginate.
Try this:
#user.posts(:limit => 10, :order => "created_at DESC")
http://guides.rubyonrails.org/active_record_querying.html
You should take a look at the options available for the has_many association.
You could try something like this:
class User < ActiveRecord::Base
has_many :posts
has_many :top_ten_posts, { :class_name => "Post", :order => :some_rating_column, :limit => 10 }
end