I'm trying to merge three Active Record arrays in a Rails 5 app so that I have a nice collection of jobs, forum threads and blogs on my home page.
I have the following code:
application_controller.rb
def home
#blogs = Blog.limit(6)
#jobs = Job.where(approved: true).limit(6)
#forum_threads = ForumThread.includes(:forum_posts).limit(6)
#everything = #blogs + #jobs + #forum_threads
end
home.html.erb
<% #everything.sort_by(&:created_at).reverse.each do |item| %>
<% if item.is_a?(Job) %>
<%= render partial: "application/partials/home_job", locals: {item: item} %>
<% elsif item.is_a?(ForumThread) %>
<%= render partial: "application/partials/home_forum", locals: {item: item} %>
<% elsif item.is_a?(Blog) %>
<%= render partial: "application/partials/home_blog", locals: {item: item} %>
<% end %>
<% end %>
The problem I'm having is that this code doesn't display the records in date order by created_by, instead I have a rather random collection of jobs, forum threads and blogs starting at a seemingly random date.
If I add, say, a new job, it doesn't appear in the collection displayed on /home page. However, if I delete all records from the db and start adding new records then the code works fine and displays the posts in the correct order with the behaviour I expect.
I can't push this code live to Heroku because I can't delete all the records that already exist in production. It's almost like there's some kind of cache that needs clearing out. Does anyone know what's going on?
#blogs = Blog.order(created_at: :desc).limit(6)
etc.
Problem 1: Getting the right records from the database
Option A: If you will always be sorting each model by the created_at value (a common desire), add a default_scope to each model (Rails 4+ version below). Your limit calls in the controller will automatically take advantage of the default scope.
app/models/blog.rb
class Blog < ActiveRecord::Base
default_scope { order created_at: :desc }
...
end
Option B: If you only do this in certain circumstances, but you do it for several models, I like to extract that into a Timestamped module (below). You will need to use the most_recent method in your controller when extracting records from the database to ensure you're getting the most recent ones.
app/models/concerns/timestamped.rb
module Timestamped
extend ActiveSupport::Concern
included do
scope :most_recent, -> { order created_at: :desc }
scope :least_recent, -> { order created_at: :asc }
scope :most_fresh, -> { order updated_at: :desc }
scope :least_fresh, -> { order updated_at: :asc }
end
end
class Blog < ActiveRecord::Base
include Timestamped
...
end
Problem 2: Sorting the array
Even with a simple case like this, I'd recommend adding an array extension that matches the most_recent method that timestamped.rb defines for ActiveRecord::Relations.
lib/array_extensions.rb
class Array
def most_recent
sort { |a, b| b.created_at <=> a.created_at }
end
end
and then require the extension with an initializer:
config/initializers/extensions.rb
require 'array_extensions'
Problem 3: Keeping the controller clean.
Generally each controller action should only set up one instance variable, and in this case it looks like you are not even using the #blogs, #jobs, and #forum_threads variables in the views. Vivek's answer solves this, although I'd do the flattening and sorting logic in the controller:
def home
#posts = Blog.most_recent.limit(6) + Job.approved.most_recent.limit(6) + ForumThread.most_recent.includes(:forum_posts).limit(6)
#posts = #posts.most_recent
end
Problem 4: Minimize if/then logic in your view
Instead of this:
<% #everything.sort_by(&:created_at).reverse.each do |item| %>
<% if item.is_a?(Job) %>
<%= render partial: "application/partials/home_job", locals: {item: item} %>
<% elsif item.is_a?(ForumThread) %>
<%= render partial: "application/partials/home_forum", locals: {item: item} %>
<% elsif item.is_a?(Blog) %>
<%= render partial: "application/partials/home_blog", locals: {item: item} %>
<% end %>
<% end %>
Do this:
<% #everything.sort_by(&:created_at).reverse.each do |item| %>
<%= render "application/partials/home_#{item.class.name.underscore}", item: item %>
<% end %>
And make sure your partials are named appropriately
You can do like this:
def home
#collections=[]
#collections << Blog.limit(6)
#collections << Job.where(approved: true).limit(6)
#collections << ForumThread.includes(:forum_posts).limit(6)
end
<% #collections.flatten.sort_by(&:created_at).reverse.each do |item| %>
....iteration here ....
<% end %>
if i understood your question correctly, you want to sort the array after you merged it by date. I would do it like that:
#everything = #everything.sort {|x| x.created_at }
Hope that helps.
Related
I have an Orders index page with two links, whose indices I want to filter by the status of the order:
<%= link_to "Current Orders", orders_path(:by_status => "processing") %>
...
<%= link_to "Past Orders", orders_path(:by_status => "completed") %>
My controller looks like:
class OrdersController < ApplicationController
has_scope :by_status
def index
case params[:status]
when "completed"
#past_orders = Order.where(status: "completed")
when "processing"
#current_orders = Order.where(status: "processing")
end
end
end
I'm sure def index is the main problem. But I also can't figure out how to display that in the view page. I have:
<% #past_orders.each do |order| %>
I would appreciate the help.
to solve your problem you can split the render based of your condition from index.html.erb
in index.html.erb create condition if #post_orders has contents then render past_orders else render current_order
<% if #post_orders %>
<%= render 'past_order.html.erb' %>
<% else %>
<%= render 'current_order.html.erb' %>
<% end %>
then you create two partial file with name _past_order.html.erb and _current_order.html.erb put in same folder with index.html.erb
If you want the view to look the same for both scopes then you should handle it at the controller. It looks like you're using the has_scope gem, so this should work:
class OrdersController < ApplicationController
has_scope :by_status, only: :index
def index
#orders = apply_scopes(Order)
end
end
You need to have a matching scope method on the Order model
class Order < ApplicationRecord
scope :by_status, ->(status) { where status: status }
end
In your view orders/index.html.erb you would handle the collection exactly the same way, using #orders for both current and past orders
<% #orders.each do |order| %>
If you ever need to display a component of the view differently depending on the order status just add an if statement
<% if order.status == "completed" >
<p>Something<p>
<% else >
<p>Something else<p>
<% end >
I need a little advice about the join and includes methods.
I display a list of groups in the index view. Each has a modal associated, and, in this modal, I would like to display the requests associated to this group. Normally, I'd use #requests = group.requests, but would like to use join for sending just one request to my database.
Since I'm in the index view, I don't have a #group in my action.
controller:
def index
#groups = current_user.groups
end
view (index):
<% #groups.each do |g| %>
<MODAL>
<% #requests = g.requests %>
<% #requests.each do |r| %>
<%= r.date %>
<% end %>
</MODAL>
<% end %>
I guess I can also use join and include for #groups, but there is already one SQL request, so I'm good with it.
In your controller, add includes like this to preload requests and avoid n+1 queries.
def index
#groups = current_user.groups.includes(:requests)
end
View is fine, but you can also write as:-
<% #groups.each do |g| %>
<MODAL>
<% g.requests.each do |r| %>
<%= r.date %>
<% end %>
</MODAL>
<% end %>
I'm trying to loop over my 'offers' collection in a partial, but each 'offer' has a column 'featured' which is a boolean which defaults to false. I'm trying to loop over the collection and only display the offers which have the featured column set to true.
I currently have:
<%= render #offers %>
Trying below but comes back with 'undefined method 'featured'
<%= render #offers if #offer.featured == true %>
Any help would be fantastic
In your controller, set up another collection:
#featured_offers = Offer.where(featured: true)
And render that instead:
<%= render #featured_offers %>
To correct your immediate code, you're calling .featured on #offer - which doesn't exist.
You'll either need to loop through #offers and use logic on offer, or use conditions inside the partial (which is highly inefficient):
<% #offers.each do |offer| %>
<%= render offer if offer.featured %>
<% end %>
or
<%= render #offers %>
#_offer.html.erb
<% if offer.featured %>
This is super inefficient
<% end %>
--
#jason is correct with his recommendation of using a where clause
You may even want to go a step further and set up a scope:
#app/models/offer.rb
class Offer < ActiveRecord::Base
scope :featured, -> { where featured: true }
end
#offers = Offer.featured
You can even chain the scope:
#offers = Offer.where(user_id: params[:id])
<%= render #offers.featured %>
For about a week now I have been trying to get a view to render. I have an application that needs to be able to export collections so I decided to use a line partial that renders as a .txt and .csv in the web browser. So far so good in terms of getting the entire collection to render (line by line). However, I am having trouble getting certain collection objects (in this case products) to duplicate themselves based on a certain attribute (size element).
The code below is kind of where I am stuck at now
Controller
class PexportController < ApplicationController
layout 'csv'
def index
end
def show
#feed_template = params[:id]
#products = Product.find :all
#products.each do |product|
unless product.size.nil? || product.size.empty? || product.size.kind_of?(Fixnum)
#products << new_products_for(product)
end
end
respond_to do |format|
format.html
format.text
end
end
private
def new_products_for(product = {})
products = Array.new
product.size.each do |p|
products << Product.new(p.attributes)
end
products
end
end
View
<%= render partial: 'pexport/p', collection: #products %>
Partial
<%= p.sku %> <%= p.name %> <%= p.price %> ......
I basically just need to get the controller method to work. The attribute :size that I am using for the line duplicator is simply an array like so [1,2,3]. And I would like products that contain this size attribute to duplicate themselves based on the number of sizes in their size array. I am not even sure if I am going about it the right away but it has gotten to that point where I am going in circles so I figured I would post it.
Alternative answer: is there some reason you need to duplicate the entire object in the controller? You could simplify things by just doing something like this in your view:
<% if p.size.is_a?(Array) %>
<% p.size.each do |s| %>
<%= p.sku %> <%= p.name %> <%= p.price %> <%= s %>
<% end %>
<% else %>
<%= p.sku %> <%= p.name %> <%= p.price %> <%= p.size %>
<% end %>
Or something to that effect.
If I understand what you're doing, you have a list of products, but some of those product entries should be displayed as more than one product if they have more than one size. Assuming that's correct, your logic is a bit off: new_products_for is returning an array which is being added as a single element at the end of your #products array. So your partial won't know how to deal with it. You could try something like this:
#my_products = Product.find :all
#products = []
#my_products.each do |p|
if p.size.blank? || p.size.kind_of?(Fixnum)
#products << p
else
#products += new_products_for(p)
end
end
Also, I suggest you make the Product.new line more explicit:
products << Product.new(:sku => p.sku, :name => p.name, ...)
p.attributes will give you all the attributes of the model, including id, created_at, updated_at which may interfere with what you're doing.
How can I show recent added #post and #photos in one list? For example:
post - LAlala (10.10.2011)
photos - [] [] [] [] (1.1.2011)
post - Bbbdsfbs (2.12.2010)
post - Lasdasdf2 (2.10.2009)
#posts = Post.limit(20).order('created_at desc')
#photos = Photo.limit(20).order('created_at desc')
#recent_items = (#posts + #photos).sort_by(&:created_at)
<% #recent_items.each do |item| %>
<% if item.class == "Photo" %>
<%= image_tag item.url %>
<% else %>
<%= item.title %>
<% end %>
<% end %>
Alternatively, use group_by to do it like this:
#recent_items = (#posts + #photos).group_by(&:created_at)
<% #recent_items.each do |date, items| %>
Date: <%= date %>
<% items.each do |item| %>
Show information here.
<% end %>
<% end >
I would move the view logic into a helper if I were you for DRYer code.
It is much better to do this is the database.
I just say this: polymorphism + database views.
Create a database view which contains the columns you need from both Post and Photo, including the column "type" containing a the name of the model (you need it for the polymorphism). Call this view for example "list_items". Then create a model called "ListItem". Then you can use this model like any other, paginate it and whatever you need to do.
ListItem.order("created_at > ?", Date.yesterday).page(params[:page])
And don't forget to configure the polymorphic association
However, all this is much easier to accomplish with the listable gem. Check it out!