0

In my Rails 4 app, I have a Post and a Calendar model: a calendar has_many posts and a post belong_to a calendar.

In the post show.html.erb view, located at /posts/:id, I want to allow users to navigate back and forth between the posts of the calendar to which the current post belongs to, with a "previous post" button and a "next post" button.

Here are my routes:

resources :calendars do
    resources :posts, shallow: true
  end
end

I know I will have something like that in my post show.html.erb view:

<% if @calendar.posts.count > 1 %>
  <%= link_to "< Previous", @previous_post %> | Post Preview | <%= link_to "Next >", @next_post %>
<% else %>
  Post Preview
<% end %>

So far, in my PostsController, I came up with:

def show
  @calendar = Calendar.find_by_id(@post.calendar_id)
  @posts = @calendar.posts
  @previous_post = @post.previous
  @next_post = @post.next
end

However, I am struggling to come up with the right definition of the previous and next methods (that you can see in the PostsController code above).

These methods must allow me to find — respectively — the post that is right before or right after the current post in @calendar.posts

How can I achieve that?

Thibaud Clement
  • 6,607
  • 10
  • 50
  • 103

4 Answers4

4

Your solution with next and previous commands relative to date is good, but doesn't work completely because you need to order the results chronologically as well. The where will filter the ones you don't want, but you need to make sure that the rest are in the order you desire.

So you'd have something like:

def next
  calendar.posts.where("time > ?", time).order(:time).first
end

def previous
  calendar.posts.where("time < ?", time).order(time: :desc).first
end

Edit: I'm assuming that time is a DateTime. If it is a Time ONLY without date information, you'll urgently want to change that to a DateTime field.

Rich Seviora
  • 1,789
  • 12
  • 16
  • 1
    Thanks a lot for your answer. I did not know I had to order the records, but that makes a lot of sense. However, unfortunately, I am still not getting the behavior I am looking for. It seems the records are sorted by `time`, but regardless of the `date`, which is a problem. I thought time also included date, doesn't it? Is there a way to order the record by date and then by time? (Note: several records can have the same `date`, but not the same `time`, this is the reason why I went with `time` instead of `date`.) – Thibaud Clement Oct 30 '15 at 03:54
  • I just tried a new piece of code and shared in the **UPDATE 2** section of my answer. Let me know what you think of it? – Thibaud Clement Oct 30 '15 at 04:01
  • Thanks! I've amended my edit. You need to have your date time information together in one field. Anything else is a recipe for needless disaster :) – Rich Seviora Oct 30 '15 at 04:04
1

Not a very ideal kind of solution, but in your case, should work and give you the previous and next posts of a @post from a @posts array.

Add get_next_previous_posts helper method and use it in your controller's show method:

  def show
    @calendar = Calendar.find_by_id(@post.calendar_id)
    @posts = @calendar.posts
    @previous_post, @next_post = get_next_and_previous_posts(@posts, @post)
  end

  private

  def get_next_and_previous_posts(posts, current_post)
    next_post = posts.detect { |p| p.id > current_post.id }
    prev_post = posts.reverse.detect { |p| p.id > current_post.id }
    [prev_post, next_post]
  end
K M Rakibul Islam
  • 33,760
  • 12
  • 89
  • 110
0

It sounds like you need some pagination. will_paginate or kaminari gems should do the trick (I prefer kaminari).

baron816
  • 701
  • 4
  • 13
  • Thanks for your answer. Don't take this the wrong way (newbie here), but is it worth implementing pagination to display one record per page? I would have thought that pagination was useful to filter records with a specific gap, like 10 by 10 for instance. – Thibaud Clement Oct 28 '15 at 01:32
  • 1
    Yeah I think so. You need to keep track of which record you're on and which one is next is a little tricky, and probably going to take more code and more time than it's worth to implement. Kaminari is really easy to plug in, especially since AJAX is easy with it and you can have a simple "Next Page" button. – baron816 Oct 28 '15 at 01:39
  • Ok thanks for your explanation. I will keep digging and see if there is a Rails way to achieve what I need. If there is not, I will go with one of the gems you recommend ;) Thanks a lot again. – Thibaud Clement Oct 28 '15 at 01:41
  • 1
    While we're at it, I am of the opinion that you shouldn't pass more than one instance variable from your controller to your view. This was an epiphany to me: https://robots.thoughtbot.com/sandi-metz-rules-for-developers – baron816 Oct 28 '15 at 01:42
  • Thanks a lot for this very interesting link. A friend mentioned the "one instance variable per controller rule" to me a week ago and I am still trying to figure out how to comply with it. – Thibaud Clement Oct 28 '15 at 01:49
  • 1
    To answer this question, you'd want to use methods on the instance variable to get at the other values. So instead of using `@calendar`, `@post`, `@previous_post`, `@nex_post`, you'd have `@post`, `@post.calendar`, `@post.previous`, `@post.next`. – Rich Seviora Oct 30 '15 at 04:08
  • Ok, thanks a lot. And, if I may ask, where should I place these methods: post model, post helper, post controller, post view, somewhere else? – Thibaud Clement Oct 30 '15 at 16:12
  • In the example I've given they'd have to be part of the `Post` model. They'd also make the most sense there, because otherwise the helper method (the second best option) would spend all of its time looking at the `Post` model. Feature Envy is a 'code smell', which you can read more about here: https://sourcemaking.com/refactoring/smells/feature-envy – Rich Seviora Oct 31 '15 at 03:57
0

This is what I ended up doing:

post.rb

def next
  calendar.posts.where("id > ?", id).first
end

def previous
  calendar.posts.where("id < ?", id).last
end

posts_controller.rb

def show
  @calendar = Calendar.find_by_id(@post.calendar_id)
  @previous_post = @post.previous
  @next_post = @post.next
end

This is not an ideal solution, but it is working.

—————

UPDATE:

Because posts must be displayed in chronological order, I had to change the above code to:

#post.rb

    def next
      calendar.posts.where("time > ?", time).first
    end

    def previous
      calendar.posts.where("time < ?", time).last
    end

However, this is not working perfectly, as you can see on this gif:

enter image description here

It is almost as if the previous button still works based on post id and not time.

I did restart my server in case that was the problem, but it did not fix it.

Any idea how I can improve on this code?

—————

UPDATE 2: based on Richard Seviora's answer, I also tried this:

#post.rb

def next
    calendar.posts.where("date > ? AND time != ?", date, time).order(:time).first
  end

  def previous
    calendar.posts.where("date < ? AND time != ?", date, time).order(time: :desc).first
  end

Still not working as expected.

—————

Thibaud Clement
  • 6,607
  • 10
  • 50
  • 103
  • 1
    Yeah, not ideal but works. My code essentially does the same thing, but not using ActiveRecord but using Ruby :-) – K M Rakibul Islam Oct 30 '15 at 01:59
  • Indeed. Actually, I had to change the code I posted in my answer, to use time instead of id, because posts must not be displayed based on their id but based on the date / time (in the calendar). I am still struggling with one thing though, as explained in my updated answer. If you have an idea to make it work, I am happy to validate your answer then ;) – Thibaud Clement Oct 30 '15 at 02:51
  • This is a good step, but it won't work because Date and Time need to be evaluated as one entity. Otherwise, if your post is at 5PM on 2015-10-25, it would miss the post at **4**PM on 2015-10-**26**. It would also miss 6PM on 2015-10-25 because the date is still the same. – Rich Seviora Oct 30 '15 at 04:06
  • That is exactly what is happening. So the best way to fix it is to merge the `date` and `time` attributes into one `datetime` attribute, and then call whichever part I need (`date` or `time`) from it when I need it, right? – Thibaud Clement Oct 30 '15 at 06:07
  • Yes, exactly :) *Always, always, always* look at date and time together because they are part of one continuum. It'd be like looking at whole numbers and decimals separately. :) – Rich Seviora Oct 30 '15 at 15:19
  • 1
    Thanks a lot for your guidance. Very helpful! I am still fairly new to programming in general and Rails in particular, so I am learning from this rookie mistake and will keep it in mind from now on. – Thibaud Clement Oct 30 '15 at 15:39
  • Just for information, I found this thread and thought it could be interesting: http://stackoverflow.com/questions/1261329/whats-the-difference-between-datetime-and-time-in-ruby – Thibaud Clement Oct 30 '15 at 16:11
  • Interesting. I didn't know about the performance differences. Thanks! :) Could you also mark my response as the answer? Also, glad to be of help :) – Rich Seviora Oct 31 '15 at 03:58