Ruby on Rails: Display objects by most recent in the view? - ruby-on-rails

I have this method at the moment:
<% for comment in #critical_process.review.comments %>
<div id="comment">
<%=h comment.comment %> <br/>
<span id="comment_email">
By: <%=h comment.user.email%>
</span>
</div>
<% end %>
however I need it to display the comments in order of the most recently updated at the top and work its way down.
Thanks

Assuming the Comment model has an updated_at column, and you are using Rails 3, you can just tell ActiveRecord to order the Comment records appropriately as follows:
<% for comment in #critical_process.review.comments.order('updated_at DESC') %>
The Rails 2.x equivalent would be:
<% for comment in #critical_process.review.comments.all(:order => 'updated_at DESC') %>
Whilst this will work perfectly well, it's generally considered best to move most of your query generation into the controller, in which case you might do the following in the controller:
#comments = #critical_process.review.comments.order('updated_at DESC')
... and then iterate over the #comments collection in the view.

Related

how to add uniq in a query

in view I have this
<div>
<% #readers.each do |reader| %>
<% user_profile_url = user_url(reader.user.username) %>
<div>
<%= link_to user_profile_url do %>
<%= reader.user.username %>
<% end %>
<span>[<%= reader.created_at.to_date %>]</span>
</div>
<% end %>
</div>
in the books_controller i have this
def readers_list
#book = Book.find(params[:id])
#readers = #book.downloads.order(created_at: :desc)
end
I have this problem: If an user download twince a book, in this list I see the username twince.
So, I know, I have to add uniq
I tried to add it at the end of the #readers, but it didn't work. How (and where) to add it?!
I also tried to make this #readers = #book.downloads.where(user_id: params[:user_id]).order(created_at: :desc), but it didn't work.
Use DISTINCT instead uniq. As you'll get everything and then do uniq, that's often considered as inefficient:
book
.downloads
.select('distinct on (downloads.user_id) downloads.*')
.order('downloads.user_id, downloads.created_at DESC')
downloads was scoped to a JOIN clause, so the created_at column was present in both tables, hence the explicit reference in both select and order.
Another alternative is to use "Group By", In rails it can be done like this
#book.downloads.order(created_at: :desc).group(:user_id)
If you want to check What's faster, 'DISTINCT' or 'GROUP BY' then here is a good place to get started.

Rails 4, how to ordering using custom method

I want to order the Conversation model, using a custom method.
I found some solution:
How do you order by a custom model method that has no attribute in SQL?
and
http://awaxman11.github.io/blog/2013/10/11/sorting-a-rails-resource-based-on-a-calculated-value/ ,
but Conversation order have priority.
First- answer_percent desc,
second- order to last_answer time
(using custom model method last_answered_to_i ).
last_answered_to_i method source:
def last_answered_to_i
if Conversation.where(company_id: self.id, is_answered: true).present?
last_conversation = Conversation.where(company_id: self.id, is_answered: true).first
if last_conversation.answered_at.blank? || last_conversation.asked_at.blank?
minutes = (Time.now- last_conversation.updated_at)/1.minutes
else
minutes = (last_conversation.answered_at - last_conversation.asked_at)/1.minutes
end
minutes.to_i
else
nil
end
end
after ordering I want add pagination using kaminari gem.
#lists = Company.searchable.order("answer_percent desc").page(params[:page]).per(20)
How do I order by column and custom method and add pagination?
I think the answer depends on what you want to see in the view because some of the problem could actually be solved in how you call #lists there. Also, some of the links you found make sorting by a model method sound more difficult than it is.
In your case, you can sort your conversations by a custom method like so:
Conversation.all.sort_by(&:custom_method)
Or specifically:
Conversation.all.sort_by(&:last_answered_to_i)
Specifically, you cannot use SQL to sort or order by something not in the actual database, so you use the Ruby sort_by method. For more info on the ampersand, see this post.
For your actual view, I'm not sure really how you want to organize it. I recently did something where I needed to group my resource by another resource called "categories", and then sort the original resource by "netvotes" which was a custom model method, then order by name. I did it by:
Ordering by name in the controller: #resources = Resource.order(:name)
Grouping by category in the outer loop of the view: <% #resources.group_by(&:category).each do |category, resources| %>
Then sorting the resources by votes in the partial for resources: <%= render resources.sort_by(&:netvotes).reverse %>
The view is a bit confusing, so here is the full view loop in index.html.erb:
<% #resources.group_by(&:category).each do |category, resources| %>
<div class="well">
<h3 class="brand-text"><%= category.name %></h3>
<%= render resources.sort_by(&:netvotes).reverse %>
</div>
<% end %>
And here is the _resource.html.erb partial:
<div class="row resource">
<div class="col-sm-2 text-center">
<div class="vote-box">
<%= link_to fa_icon('chevron-up lg'), upvote_resource_path(resource), method: :put %><br>
<%= resource.netvotes %><br>
<%= link_to fa_icon('chevron-down lg'), downvote_resource_path(resource), method: :put %>
</div>
</div>
<div class="col-sm-10">
<%= link_to resource.name, resource.link, target: "_blank" %>
<p><%= resource.notes %></p>
</div>
</div>
I hope that helps you think through some more ways to address your problem.

Nested comments from scratch

Let's say I have a comment model:
class Comment < ActiveRecord::Base
has_many :replies, class: "Comment", foreign_key: "reply_id"
end
I can show a comment instance`s replies in a view like so:
comment.replies do |reply|
reply.content
end
However, how do I loop through the replies of the reply? And its reply? And its reply ad infitum? I'm feeling we need to make a multidimensional array of the replies via class method and then loop through this array in the view.
I don't want to use a gem, I want to learn
It seems like what you have is one short step away from what you want. You just need to use recursion to call the same code for each reply as you're calling for the original comments. E.g.
<!-- view -->
<div id="comments">
<%= render partial: "comment", collection: #comments %>
</div>
<!-- _comment partial -->
<div class="comment">
<p><%= comment.content %></p>
<%= render partial: "comment", collection: comment.replies %>
</div>
NB: this isn't the most efficient way of doing things. Each time you call comment.replies active record will run another database query. There's definitely room for improvement but that's the basic idea anyway.
Would using a nested set still count as 'from scratch'?
The short description of a nested set is a database-specific strategy of querying hierarchies by storing/querying pre- and post-order tree traversal counts.
A picture is worth a thousand words (see also, the wikipedia page on nested sets).
There are a bunch of nested set gems, and I can personally speak for the quality of Awesome Nested Set and Ancestry
Then, Awesome Nested Set (I know from experience, presumably Ancestry too) provide helpers to do a single query to pull up all records under a tree, and iterate through the tree in sorted depth-first order, passing in the level while you go.
The view code for Awesome Nested Set would be something like:
<% Comment.each_with_level(#post.comments.self_and_descendants) do |comment, level| %>
<div style="margin-left: <%= level * 50 %>px">
<%= comment.body %>
<%# etc %>
</div>
<% end %>
I just made that up from vague memories, and it's been a while, so this is where it can be "an exercise for the reader"
My approach is to make this done as efficient as possible.
First lets address how to do that:
DRY solution.
Least Number of queries to retrieve the comments.
Thinking about that, I have found that most of the people address the first but not the second.So lets start with the easy one.
we have to have partial for the comments so referencing the answer of jeanaux
we can use his approach to display the comments and will update it later in the answer
<!-- view -->
<div id="comments">
<%= render partial: "comment", collection: #comments %>
</div>
<!-- _comment partial -->
<div class="comment">
<p><%= comment.content %></p>
<%= render partial: "comment", collection: comment.replies %>
</div>
We must now retrieve those comments in one query if possible so we can just do this in the controller. to be able to do this all comments and replies should have a commentable_id (and type if polymorphic) so that when we query we can get all comments then group them the way we want.
So if we have a post for example and we want to get all its comments we will say in the controller.
#comments = #post.comments.group_by {|c| c.reply_id}
by this we have comments in one query processed to be displayed directly
Now we can do this to display them instead of what we previously did
All the comments that are not replies are now in the #comments[nil] as they had no reply_id
(NB: I don like the #comments[nil] if anyone has any other suggestion please comment or edit)
<!-- view -->
<div id="comments">
<%= render partial: "comment", collection: #comments[nil] %>
</div>
All the replies for each comment will be in the has under the parent comment id
<!-- _comment partial -->
<div class="comment">
<p><%= comment.content %></p>
<%= render partial: "comment", collection: #comments[comment.id] %>
</div>
To wrap up:
We added an object_id in the comment model to be able to retrieve
them( if not already there)
We added grouping by reply_id to
retrieve the comments with one query and process them for the view.
We added a partial that recursively displays the comments (as
proposed by jeanaux).
It seems like you need a self-referential association. Check out the following railscast: http://railscasts.com/episodes/163-self-referential-association
We've done this:
We used the ancestry gem to create a hierarchy-centric dataset, and then outputted with a partial outputting an ordered list:
#app/views/categories/index.html.erb
<% # collection = ancestry object %>
<%= render partial: "category", locals: { collection: collection } %>
#app/views/categories/_category.html.erb
<ol class="categories">
<% collection.arrange.each do |category, sub_item| %>
<li>
<!-- Category -->
<div class="category">
<%= link_to category.title, edit_admin_category_path(category) %>
<%= link_to "+", admin_category_new_path(category), title: "New Categorgy", data: {placement: "bottom"} %>
<% if category.prime? %>
<%= link_to "", admin_category_path(category), title: "Delete", data: {placement: "bottom", confirm: "Really?"}, method: :delete, class: "icon ion-ios7-close-outline" %>
<% end %>
<!-- Page -->
<%= link_to "", new_admin_category_page_path(category), title: "New Page", data: {placement: "bottom"}, class: "icon ion-compose" %>
</div>
<!-- Pages -->
<%= render partial: "pages", locals: { id: category.name } %>
<!-- Children -->
<% if category.has_children? %>
<%= render partial: "category", locals: { collection: category.children } %>
<% end %>
</li>
<% end %>
</ol>
We also made a nested dropdown:
#app/helpers/application_helper.rb
def nested_dropdown(items)
result = []
items.map do |item, sub_items|
result << [('- ' * item.depth) + item.name, item.id]
result += nested_dropdown(sub_items) unless sub_items.blank?
end
result
end
That can be solved with resursion or with a special data structure. Recursion is simpler to implement, whereas a datastructure like the one used by the nested_set gem is more performant.
Recursion
First an example how it works in pure Ruby.
class Comment < Struct.new(:content, :replies);
def print_nested(level = 0)
puts "#{' ' * level}#{content}" # handle current comment
if replies
replies.each do |reply|
# here is the list of all nested replies generated, do not care
# about how deep the subtree is, cause recursion...
reply.print_nested(level + 1)
end
end
end
end
Example
comments = [ Comment.new(:c_1, [ Comment.new(:c_1a) ]),
Comment.new(:c_2, [ Comment.new(:c_2a),
Comment.new(:c_2b, [ Comment.new(:c_2bi),
Comment.new(:c_2bii) ]),
Comment.new(:c_2c) ]),
Comment.new(:c_3),
Comment.new(:c_4) ]
comments.each(&:print_nested)
# Output
#
# c_1
# c_1a
# c_2
# c_2a
# c_2b
# c_2bi
# c_2bii
# c_2c
# c_3
# c_4
And now with recursive calls of Rails view partials:
# in your comment show view
<%= render :partial => 'nested_comment', :collection => #comment.replies %>
# recursion in a comments/_nested_comment.html.erb partial
<%= nested_comment.content %>
<%= render :partial => 'nested_comment', :collection => nested_comment.replies %>
Nested Set
Setup your database structure, see the docs: http://rubydoc.info/gems/nested_set/1.7.1/frames That add the something like following (untested) to your app.
# in model
acts_as_nested_set
# in controller
def index
#comment = Comment.root # `root` is provided by the gem
end
# in helper
module NestedSetHelper
def root_node(node, &block)
content_tag(:li, :id => "node_#{node.id}") do
node_tag(node) +
with_output_buffer(&block)
end
end
def render_tree(hash, options = {}, &block)
if hash.present?
content_tag :ul, options do
hash.each do |node, child|
block.call node, render_tree(child, &block)
end
end
end
end
def node_tag(node)
content_tag(:div, node.content)
end
end
# in index view
<ul>
<%= render 'tree', :root => #comment %>
</ul>
# in _tree view
<%= root_node(root) do %>
<%= render_tree root.descendants.arrange do |node, child| %>
<%= content_tag :li, :id => "node_#{node.id}" do %>
<%= node_tag(node) %>
<%= child %>
<% end %>
<% end %>
<% end %>
This code is from an old Rails 3.0 app, slightly change and untested. Therefore it will probably not work out of the box, but should illustrate the idea.
This will be my approach:
I have a Comment Model and a Reply model.
Comment has_many association with Reply
Reply has belongs_to association with Comment
Reply has self referential HABTM
class Reply < ActiveRecord::Base
belongs_to :comment
has_and_belongs_to_many :sub_replies,
class_name: 'Reply',
join_table: :replies_sub_replies,
foreign_key: :reply_id,
association_foreign_key: :sub_reply_id
def all_replies(reply = self,all_replies = [])
sub_replies = reply.sub_replies
all_replies << sub_replies
return if sub_replies.count == 0
sub_replies.each do |sr|
if sr.sub_replies.count > 0
all_replies(sr,all_replies)
end
end
return all_replies
end
end
Now to get a reply from a comment etc:
Getting all replies from a comment: #comment.replies
Getting the Comment from any reply: #reply.comment
Getting the intermediate level of replies from a reply: #reply.sub_replies
Getting all levels of replies from a reply: #reply.all_replies
I've had various generally bad experience with the different hierarchy gems available for ActiveRecord. Typically you do not want to do this yourself as your queries will end up being very inefficient.
The Ancestry gem was ok, but I had to move away from it because 'children' is a scope and NOT an association. This means you CANNOT use nested attributes with it because nested attributes only work with associations, not scopes. That may or may not be a problem depending on what you are doing, such as ordering or updating siblings through the parent or updating entire subtrees/graphs in a single operation.
The most efficient ActiveRecord gem for this is the Closure Tree gem and I had good results with it, with the caveat that splatting/ mutating entire sub-trees was diabolical because of the way ActiveRecord works. If you don't need to compute things over a tree when doing updates then it is the way to go.
I've since moved away from ActiveRecord to Sequel and it has recursive common table expression (RCTE) support which is used by its built-in tree plugin. An RCTE tree is as fast as is theoretically possible to update (just modify a single parent_id as in a naive implementation) and querying is also typically orders of magnitude faster than other approaches because of the SQL RCTE feature it uses. It is also the most space efficient approach since there is just parent_id to maintain. I am not aware of any ActiveRecord solutions that support RCTE trees because ActiveRecord doesn't cover nearly as much of the SQL spectrum that Sequel does.
If you're not wedded to ActiveRecord then Sequel and Postgres is a formidable combination IMO. You will find out the deficiencies in AR when your queries become ever so slightly complex. There is always pain moving to another ORM as its not the out of the box stock rails approach but I have been able to express queries that I couldn't do with ActiveRecord or ARel (even though they were pretty simple), and generally improved query performance across the board 10-20 times over what I was getting with ActiveRecord. In my use case with maintaining trees of data its hundreds of times faster. That means tens to hundreds times less server infrastructure I need for the same load. Think about it.
You'd collect the reply's replies within each Reply iteration.
<% comment.replies do |reply| %>
<%= reply.content %>
<% reply_replies = Post.where("reply_id = #{reply.id}").all %>
<% reply_replies .each do |p| %>
<%= p.post %>
<% end
<% end %>
Though im not sure if it'd be the most conventional way cost-wise.

How do you order by a custom model method that has no attribute in SQL?

Previously I ordered my posts as this:
#posts = Post.find(:all, :order => "created_at DESC")
But now I want to replace created_at with a custom method I wrote in the Post model that gives a number as its result.
My guess:
#posts = Post.find(:all, :order => "custom_method DESC")
which fails..
It fails because you are asking your db to do the sorting.
#posts = Post.all.sort {|a,b| a.custom_method <=> b.custom_method}
Note that this becomes non-trivial when you want to start paging results and no longer wish to fetch .all. Think about your design a bit before you go with this.
Just to expand on #Robbie's answer
Post.all.sort_by {|post| post.custom_method }.reverse
As the first answer noted, order is an Active Record command that essentially does a SQL query on your database, but that field doesn't actually exist in your database.
As someone else commented, you can more cleanly run the Ruby method sort_by by using the ampersand (more info here):
Post.all.sort_by(&:custom_method)
However, things do get complicated depending on what you want to do in your view. I'll share a case I recently did in case that helps you think through your problem. I needed to group my resource by another resource called "categories", and then sort the original resource by "netvotes" which was a custom model method, then order by name. I did it by:
Ordering by name in the controller: #resources = Resource.order(:name)
Grouping by category in the outer loop of the view: <% #resources.group_by(&:category).each do |category, resources| %>
Then sorting the resources by votes in the partial for resources: <%= render resources.sort_by(&:netvotes).reverse %>
The view is a bit confusing, so here is the full view loop in index.html.erb:
<% #resources.group_by(&:category).each do |category, resources| %>
<div class="well">
<h3 class="brand-text"><%= category.name %></h3>
<%= render resources.sort_by(&:netvotes).reverse %>
</div>
<% end %>
And here is the _resource.html.erb partial:
<div class="row resource">
<div class="col-sm-2 text-center">
<div class="vote-box">
<%= link_to fa_icon('chevron-up lg'), upvote_resource_path(resource), method: :put %><br>
<%= resource.netvotes %><br>
<%= link_to fa_icon('chevron-down lg'), downvote_resource_path(resource), method: :put %>
</div>
</div>
<div class="col-sm-10">
<%= link_to resource.name, resource.link, target: "_blank" %>
<p><%= resource.notes %></p>
</div>
</div>
This is a bit more complicated than what I like but this I like to keep my sort to stay as a active record model so its bit more complicated than just
Post.all.sort_by {|post| post.custom_method }
what I do is:
ids = Post.all.sort_by {|post| post.custom_method }.map(&:ids)
Post.for_ids_with_order(ids)
this is a custom scope in the Post model
#app/models/post.rb
class Post < ApplicationRecord
...
scope :for_ids_with_order, ->(ids) {
order = sanitize_sql_array(
["position(id::text in ?)", ids.join(',')]
)
where(:id => ids).order(order)
}
...
end
I hope that this help
Well, just Post.find(:all) would return an array of AR objects. So you could use Array.sort_by and pass it a block, and since those records are already fetched, you can access the virtual attribute inside the block that sort_by takes.
RDoc: Enumerable.sort_by
Keep in mind that sort_by will return an Array, not an ActiveRecord::Relation, which you might need for pagination or some other some view logic. To get an ActiveRecord::Relation back, use something like this:
order_by_clause = Post.sanitize_sql_array(<<custom method expressed in SQL>>, <<parameters>>)
Post.all.order(Arel.sql(order_by_clause))
in rails 3 we can do this as: Post.order("custom_method DESC")
When upgrading app from rails2 to rails3

Multiple objects in a Rails form

I want to edit multiple items of my model photo in one form. I am unsure of how to correctly present and POST this with a form, as well as how to gather the items in the update action in the controller.
This is what I want:
<form>
<input name="photos[1][title]" value="Photo with id 1" />
<input name="photos[2][title]" value="Photo with id 2" />
<input name="photos[3][title]" value="Custom title" />
</form>
The parameters are just an example, like I stated above: I am not sure of the best way to POST these values in this form.
In the controller I want to something like this:
#photos = Photo.find( params[photos] )
#photos.each do |photo|
photo.update_attributes!(params[:photos][photo] )
end
In Rails 4, just this
<%= form_tag photos_update_path do %>
<% #photos.each do |photo| %>
<%= fields_for "photos[]", photo do |pf| %>
<%= pf.text_field :caption %>
... other photo fields
UPDATE: This answer applies to Rails 2, or if you have special constraints that require custom logic. The easy cases are well addressed using fields_for as discussed elsewhere.
Rails isn't going to help you out a lot to do this. It goes against the standard view conventions, so you'll have to do workarounds in the view, the controller, even the routes. That's no fun.
The key resources on dealing with multi-model forms the Rails way are Stephen Chu's params-foo series, or if you're on Rails 2.3, check out Nested Object Forms
It becomes much easier if you define some kind of singular resource that you are editing, like a Photoset. A Photoset could be a real, ActiveRecord type of model or it can just be a facade that accepts data and throws errors as if it were an ActiveRecord model.
Now you can write a view form somewhat like this:
<%= form_for :photoset do |f|%>
<% f.object.photos.each do |photo| %>
<%= f.fields_for photo do |photo_form| %>
<%= photo_form.text_field :caption %>
<%= photo_form.label :caption %>
<%= photo_form.file_field :attached %>
<% end %>
<% end %>
<% end %>
Your model should validate each child Photo that comes in and aggregate their errors. You may want to check out a good article on how to include Validations in any class. It could look something like this:
class Photoset
include ActiveRecord::Validations
attr_accessor :photos
validate :all_photos_okay
def all_photos_okay
photos.each do |photo|
errors.add photo.errors unless photo.valid?
end
end
def save
photos.all?(&:save)
end
def photos=(incoming_data)
incoming_data.each do |incoming|
if incoming.respond_to? :attributes
#photos << incoming unless #photos.include? incoming
else
if incoming[:id]
target = #photos.select { |t| t.id == incoming[:id] }
end
if target
target.attributes = incoming
else
#photos << Photo.new incoming
end
end
end
end
def photos
# your photo-find logic here
#photos || Photo.find :all
end
end
By using a facade model for the Photoset, you can keep your controller and view logic simple and straightforward, reserving the most complex code for a dedicated model. This code probably won't run out of the box, but hopefully it will give you some ideas and point you in the right direction to resolve your question.
Rails does have a way to do this - I don't know when it was introduced, but it's basically described here: http://guides.rubyonrails.org/form_helpers.html#using-form-helpers
It took a bit of fiddling to alter the configuration properly for the case where there's no parent object, but this seems to be correct (it's basically the same as gamov's answer, but cleaner and doesn't allow for "new" records mixed in with the "update" records):
<%= form_tag photos_update_path do %>
<% #photos.each do |photo| %>
<%= fields_for "photos[#{photo.id}]", photo do |pf| %>
<%= pf.text_field :caption %>
... [other fields]
<% end %>
<% end %>
<% end %>
In your controller, you'll end up with a hash in params[:photos], where the keys are photo IDs, and the values are attribute hashes.
You can use "model name[]" syntax to represent multiple objects.
In view, use "photo[]" as a model name.
<% form_for "photo[]", :url => photos_update_path do |f| %>
<% for #photo in #photos %>
<%= render :partial => "photo_form", :locals => {f => f} %>
<%= submit_tag "Save"%>
<% end %>
<% end %>
This will populate input fields just like you described.
In your controller, you can do bulk updates.
def update
Photo.update(params[:photo].keys, params[:photo].values)
...
end
Indeed, as Turadg mentioned, Rack (Rails 3.0.5) fails if you mix new & existing records in Glen's answer.
You can work around this by making fields_for work manually:
<%= form_tag photos_update_path do %>
<% #photos.each_with_index do |photo,i| %>
<%= fields_for 'photos[#{i}]', photo do |pf| %>
<%= pf.hidden_field :id %>
... [other photo fields]
<% end %>
<% end %>
This is pretty ugly if you ask me, but it's the only way I found to edit multiple records while mixing new and existing records.
The trick here is that instead of having an array of records, the params hash gets a array of hashes (numbered with i, 0,1,2, etc) AND the id in the record hash. Rails will update the existing records accordingly and create the new ones.
One more note: You still need to process the new and existing records in the controller separately (check if :id.present?)

Resources