rails rendering index in index with fragment caching - ruby-on-rails

In my rails 4 app I have comments for different models with polymorphic association. For the post model I display comments on the index page, but for product model I do it on the show page. I have problems with fine-tuning the rendering and caching on the post index page, since it's comments index in posts index. I provided my solutions both for the post version and product version.
I don't use touch:true in comment model since it doesn't make sense on the product show page. Thanks to this I don't use caching for the posts on the posts index page since the cache key would be too complex thanks to the comments.
My questions:
Is there a better way to render comments for the posts?
Is my caching strategy good enough or I should use an "outer" caching for product or post?
Post has_many :comments, as: :commentable
Product has_many :comments, as: :commentable
Comment belongs_to :commentable, polymorphic: true ###### NO touch: true
posts index
<%= render #posts %> #no caching here, key would be too complex
_post
<% cache ['post', post, post.user.profile] do %>
<%= post.user.full_name %> #delegated from profile
<%= post.body %>
<% end %>
<%= render partial: 'comments/form', locals: { commentable: post } %>
<%= render partial: 'comments/comment', collection: post.comments.ordered.includes(:user, :user_profile), as: :comment %>
products controller
def show
#product = Product.find(params[:id])
#comments = #product.comments.ordered.includes(:user, :user_profile)
end
products show
<% cache [#product, #product.user.profile] do %>
<%= product.user.full_name %> #delegated from profile
<%= product.name %>
<%= product.description %>
<% end %>
<% cache ['comments-index', #comments.map(&:id), #comments.map(&:updated_at).max,
#comments.map{ |comment| comment.user.profile.updated_at }.max] %>
<%= render #comments %>
<% end %>
_comment (same both for product and post)
<% cache ['comment', comment, comment.user.profile] do %>
<%= comment.user.full_name %> #delegated from profile
<%= comment.body %>
<% end %>

Related

Rails form - multiple nested routes undefined method '_path'

This app has the following models:
Farm (has_many :crops)
Crop (belongs_to :farm, has_many :issues)
Issue (belongs_to :crop)
Here are the routes:
resources :farms do
resources :crops do
resources :issues
end
end
I want a user to be able to create a new "issue" from the Farm#show page that lists all the farm's crops. Here is the form that is causing the error on the Farm#show page:
undefined method `crop_issues_path' for #<#:0x007fa814a3cc30>
#from the show action on the controller:
##farm = Farm.find(params[:id])
##crops = #farm.crops
<% #crops.each do |crop| %>
<%= crop.id %>
<%= form_for([crop, crop.issues.build]) do |f| %>
<%= f.select(:issue_type, options_for_select([['mold'], ['pests'], ['dehydration'], ['other']])) %>
<%= f.text_area :notes %><br>
<%= f.submit "New Issue", :class => "button" %>
<% end %>
<% end %>
My create action on issues controller:
def create
#crop = Crop.find(params[:crop_id])
#issues = #crop.issues.create(params[:issue].permit(:issue_type, :notes, :crop_id))
redirect_to :back
end
I have used nearly identical code when the crops and issues were not nested under farms, and it works. I believe the issue is because of the nesting, but cannot figure out a solution.
I think your problem is with the object you're binging the form to. It should be #farm, as you're in the #farms show action.
I modified it to this:
<% #crops.each do |crop| %>
<%= crop.id %>
<%= form_for([#farm, crop, crop.issues.build]) do |f| %>
<%= f.text_area :notes %><br>
<%= f.submit "New Issue", :class => "button" %>
<% end %>
<% end %>
with my controller like this:
class FarmsController < ApplicationController
def index
end
def show
#farm = Farm.find_by_id(params[:id])
#crops = #farm.try(:crops)
end
end

Rails 4 fields_for not displaying or updating

I have a nested relationship where dashboard has many rewards, and I am trying to add a fields_for to the page in order to edit the rewards. Unfortunately, it doesn't seem to be working and I don't know why.
Here's what I have.
Dashboard model:
class Dashboard < ActiveRecord::Base
belongs_to :manager
has_many :rewards
accepts_nested_attributes_for :rewards, allow_destroy: true
end
Rewards model:
class Reward < ActiveRecord::Base
belongs_to :dashboard
end
Dashboard controller:
class DashboardsController < ApplicationController
before_action :authenticate_manager!
# Requires user to be signed in
def index
#dashboards = Dashboard.all
end
def new
#dashboard = Dashboard.new
end
def edit
#dashboard = Dashboard.find(params[:id])
end
def create
#dashboard = Dashboard.new(dashboard_params)
#dashboard.save
if #dashboard.save
redirect_to dashboard_path(#dashboard)
else
render :action => new
end
end
def update
#dashboard = Dashboard.find(params[:id])
if #dashboard.update(dashboard_params)
redirect_to :action => :show
else
render 'edit'
end
end
def show
#dashboard = Dashboard.find(params[:id])
end
def destroy
#dashboard = Dashboard.find_by_id(params[:id])
if #dashboard.destroy
redirect_to dashboards_path
end
end
private
def dashboard_params
args = params.require(:dashboard).permit(:title, :description, :rewards, {rewards_attributes: [ :id, :title, :referralAmount, :dashboardid, :selected, :_destroy] } )
args
end
end
Form in dashboards view:
<%= form_for :dashboard, url: dashboard_path(#dashboard), method: :patch do |f| %>
<% if #dashboard.errors.any? %>
<div id="error_explanation">
<h2>
<%= pluralize(#dashboard.errors.count, "error") %> prohibited
this dashboard from being saved:
</h2>
<ul>
<% #dashboard.errors.full_messages.each do |msg| %>
<li><%= msg %></li>
<% end %>
</ul>
</div>
<% end %>
<p>
<%= f.label :title %><br>
<%= f.text_field :title %>
</p>
<p>
<%= f.label :description %><br>
<%= f.text_field :description %>
</p>
<%= f.fields_for :rewards do |reward| %>
<%= reward.label :title %><br>
<%= reward.text_field :title %>
<%= reward.check_box :_destroy %>
<%= reward.label :_destroy, "Remove reward" %>
<% end %>
<p>
<%= f.submit %>
</p>
<% end %>
I went ahead and manually added rewards to the database through the rails console and it worked beautifully, but they are not showing up on the page. They will show up if I iterate through them like so
<% if #dashboard.rewards.any? %>
<ul>
<% #dashboard.rewards.each do |reward| %>
<li><%= reward.title %></li>
<li><%= reward.referralAmount %></li>
<% end %>
</ul>
<% else %>
<p>no rewards</p>
<% end %>
However the fields_for does not display the rewards or their content and resultingly allow one to edit them.
Let me know if you need further information/code.
Try to modify your:
View:
<% if #dashboard.errors.any? %>
<div id="error_explanation">
<h2>
<%= pluralize(#dashboard.errors.count, "error") %> prohibited
this dashboard from being saved:
</h2>
<ul>
<% #dashboard.errors.full_messages.each do |msg| %>
<li><%= msg %></li>
<% end %>
</ul>
</div>
<% end %>
<%= form_for #dashboard, url: dashboard_path(#dashboard) do |f| %>
........
<% end %>
Controller (has_many relationship):
def new
#dashboard = Dashboard.new
#dashboard.rewards.build
end
private
def dashboard_params
params.require(:dashboard).permit(:title, :description,
rewards_attributes: [
:id,
:title,
:referralAmount,
:dashboardid,
:selected,
:_destroy
])
end
You don't have to set the method: patch if form.
Once you got in edit page, Rails will use the update action in controller when form submission.
To check it, run rake routes,
you will see somsthing like this:
PATCH /dashboards/:id(.:format) dashboards#update
PUT /dashboards/:id(.:format) dashboards#update
In controller you need to give build
def new
#dashboard = Dashboard.new
#dashboard.rewards.build
end
"build" is just create a new object in memory so that the view can take this object and display something, especially for a form.
Hope it helps for you
You should build object before nested form. You can add whatever you want that object.
Try it in controller;
def new
#dashboard = Dashboard.new
3.times do
#dashboard.build_reward
end
end
Try setting an "#rewards" instance variable in your dashboards edit method (where #rewards = #dashboard.rewards). Then replace :rewards with #rewards.
Edit:
I believe my initial answer is inapproriate for your exact question (while it would be helpful on say the page to show a specific dashboard and its rewards). The answers above are on the right track re:
refining your params method per #aldrien.h;
Adding #santosh dadi's suggestion of
#dashboard.rewards.build
(assuming you only want one rewards fields on a form for "new")
Finally though, to avoid making fake information for a new rewards form, adding to the top of your Dashboards model:
accepts_nested_attributes_for :rewards, reject_if: lambda {|attributes| attributes['title'].blank?}
http://guides.rubyonrails.org/form_helpers.html#nested-forms

rails4 double nested models russian-doll-caching

I have the following structure in my rails4 app for the posts. Users can comment on the post and replies can be written on the comments. I'd like to use russian-doll-caching with auto-expiring keys on the page, but I don't know how I should exactly do it in this case.
Can sby tell me how to use it in this case?
Models:
#post.rb
belongs_to :user
has_many :post_comments, dependent: :destroy
#post_comments.rb
belongs_to :user
belongs_to :post
has_many :post_comment_replies, dependent: :destroy
#post_comment_replies.rb
belongs_to :user
belongs_to :post_comments
posts/index.html.erb
<div class="post-index new-post-insert">
<%= render #posts %>
</div>
_post.html.erb
<%= post.body %>
<%= post.user.full_name %>
....
<%= render partial: 'posts/post_comments/post_comment', collection: post.post_comments.ordered.included, as: :post_comment, locals: {post: post} %>
_post_comment.html.erb
<%= post_comment.body %>
<%= post_comment.user.full_name %>
......
<%= render partial: 'posts/post_comment_replies/post_comment_reply', collection: post_comment.post_comment_replies.ordered.included, as: :post_comment_reply, locals: { post_comment: post_comment } %>
_post_comment_reply.html.erb
<%= post_comment_reply.user.full_name %>
<%= post_comment_reply.body %>
You need to do a few things
Add touch to your belongs_to relations
The children and grandchildren of Post needs to touch their parents so that the updated_at column updates which in turn invalidates the cache keys.
#post_comments.rb
belongs_to :user
belongs_to :post, touch: true
has_many :post_comment_replies, dependent: :destroy
#post_comment_replies.rb
belongs_to :user
belongs_to :post_comments, touch: true
Add the cache command to your views
posts/index.html.erb
In the main list of posts we want to cache on the latest updated_at for posts and the latest updated_at for the respective user.
<div class="post-index new-post-insert">
<% cache ["posts", #posts.maximum(:updated_at).to_i, #posts.map {|p| p.user.try(:updated_at).to_i}.max] %>
<%= render #posts %>
<% end %>
</div>
_post.html.erb
<% cache ["postlist", post, post.user] %>
<%= post.body %>
<%= post.user.full_name %>
....
<%= render partial: 'posts/post_comments/post_comment', collection: post.post_comments.ordered.included, as: :post_comment, locals: {post: post} %>
<% end %>
_post_comment.html.erb
<% cache ["postcommentlist", post_comment, post_comment.user] %>
<%= post_comment.body %>
<%= post_comment.user.full_name %>
......
<%= render partial: 'posts/post_comment_replies/post_comment_reply', collection: post_comment.post_comment_replies.ordered.included, as: :post_comment_reply, locals: { post_comment: post_comment } %>
<% end %>
_post_comment_reply.html.erb
<% cache ["postcommentreplylist", post_comment_reply, post_comment_reply.user] %>
<%= post_comment_reply.user.full_name %>
<%= post_comment_reply.body %>
<% end %>
This could be improved by using cached: true in the render partial function. However since we want to expire the cache if the user changes their username it becomes a bit tricky.
You can do that if you override all the models cache_key functions.
Why should I use cached: true in render partial?
Instead of calling cache inside each partial (like we do above) we could do
<%= render partial: 'posts/post_comments/post_comment', collection: post.post_comments, cached: true %>
If we only need to cache on the post_comment´s updated_at.
The difference between the two are that when we cache inside the partial Rails issues a get command to the cachestore (e.g. memcache) one time per object. Therefore if you have 50 postcomments, there will be 50 separate requests to memcached to retrieve all.
But if we instead use cached: true in the render call Rails will issue a multi_get request to memcached and retrieve all 50 objects in one request. Thus improving page loadtime. In tests we have conducted in our production env. it decreased the page loadtime by ~50ms - ~200ms depending on the amount of data.

How can I get data from multiple table association in rails?

I want to get data from multiple table in rails,but it is not working.
Here is my code.
Category.rb
has_many :posts
post.rb
has_many :mini_posts
belongs_to :category
mini_post.rb
belongs_to :post
controller
#posts = Category.find(params[:id]).posts.mini_posts
viewfile
<% #posts.each do |post| %>
<%= post.title %>
<%= post.description %>
<% post.mini_posts.each do |mpost| %>
<%= mpost.name %>
<%= mpost.experience %>
<% end %>
<% end %>
The error shows "undefined method `mini_posts'.
How can I solve this?
Your code is chaining methods, and returning mini posts, not eager loading the mini posts which is what I assume you want.
You want either
#posts = Post.includes(:mini_posts).where(category_id: params[:id])
Or
#category = Category.includes(posts: :mini_posts).find(params[:id])
#posts = #category.posts
Change
#posts = Category.find(params[:id]).posts.mini_posts
to
#posts = Category.find(params[:id]).posts

Ancestry Gem for Nested Comments with Rails causing undefined method error

I have been trying to fix an error associated with using the Ancestry gem for comments on my app for Rails 4. I used railscast episode 262 as a guide. However, unlike the episode, my comments model is a nested resource inside another model.Before I go further, I will supply the necessary code for reference. If you like to read the error right away, it is mentioned right after all the code snippets.
The Relevant Models:
class Comment < ActiveRecord::Base
has_ancestry
belongs_to :user
belongs_to :scoreboard
end
class Scoreboard < ActiveRecord::Base
#scoreboard model is like an article page on which users can post comments
belongs_to :user
has_many :teams, dependent: :destroy
has_many :comments, dependent: :destroy
end
Relevant code in the route file:
resources :scoreboards do
resources :comments
resources :teams, only: [:edit, :create, :destroy, :update]
end
The Scoreboards Controller Method for the page on which one can post comments:
def show
#scoreboard = Scoreboard.find_by_id(params[:id])
#team = #scoreboard.teams.build
#comment = #scoreboard.comments.new
end
The Comments Controller:
class CommentsController < ApplicationController
def new
#scoreboard = Scoreboard.find(params[:scoreboard_id])
#comment = #scoreboard.comments.new(:parent_id => params[:parent_id])
end
def create
#scoreboard = Scoreboard.find(params[:scoreboard_id])
#comment = #scoreboard.comments.new comment_params
if #comment.save
redirect_to scoreboard_url(#comment.scoreboard_id)
else
render 'new'
end
end
private
def comment_params
params.require(:comment).permit(:body, :parent_id).merge(user_id: current_user.id)
end
end
I will include the migration for the ancestry gem if any mistakes were made on that :
class AddAncestryToComments < ActiveRecord::Migration
def change
add_column :comments, :ancestry, :string
add_index :comments, :ancestry
end
end
The following code shows the view code:
Scoreboard#show View which is giving me the error in the last line:
<div class= "comment-section">
<%= form_for [#scoreboard, #comment] do |f| %>
<%= render 'shared/error_messages', object: f.object %>
<%= f.text_area :body, class: "comment-field" %>
<%= f.hidden_field :parent_id %> #is it needed to include this here? because this form is for new comments not replies
<%= f.submit "Join the discussion...", class: " comment-button btn btn-primary" %>
<% end %>
<%= nested_comments #scoreboard.comments.reject(&:new_record?).arrange(:order => :created_at) %>
</div>
The (comments partial)_comment.html.erb View:
<div class=" comment-div">
<p> Posted by <%= link_to "#{comment.user.name}", comment.user %>
<%= time_ago_in_words(comment.created_at) %> ago
</p>
<div class="comment-body">
<%= comment.body %>
<%= link_to "Reply", new_scoreboard_comment_path(#scoreboard, comment, :parent_id => comment) %>
</div>
</div>
The helper method to render comments:
def nested_comments(comments)
comments.map do |comment, sub_comment| #the comments.map also gives me an error if I choose to render the comments without the .arrange ancestry method
render(comment) + content_tag(:div, nested_comments(sub_comment), class: "nested_messages")
end.join.html_safe
end
The new.html.erb for Comments which one is redirected to for the replies form submission:
<%= form_for [#scoreboard, #comment] do |f| %>
<%= render 'shared/error_messages', object: f.object %>
<%= f.text_area :body, class: "comment-field" %>
<%= f.hidden_field :parent_id %>
<%= f.submit "Join the discussion...", class: " comment-button btn btn-primary" %>
<% end %>
Upon creating a scoreboard, I am redirected to the show page, where i get the following error:
undefined method `arrange' for []:Array
Even though the array of comments is empty, I get the same error if it wasnt. I have tried .subtree.arrange but that gives me the same error. Also, the ancestry documentation said that .arrange works on scoped classes only. I don't know what that means. I would appreciate some help on making the page work so the comments show properly ordered with the replies after their parent comments. If this is the wrong approach for threaded comments(replies and all), I would appreciate some guidance on what to research next.
.reject(&:new_record?) this will return an array. The error sounds like arrange is a scope on ActiveRecord. So move the reject to the end and it should work.
#scoreboard.comments.arrange(:order => :created_at).reject(&:new_record?)
In regards your comment nesting, I have implemented this before, and found the Railscasts recommendation of a helper to be extremely weak.
Passing parent_id to a comment
Instead, you're better using a partial which becomes recursive depending on the number of children each comment has:
#app/views/scoreboards/show.html.erb
<%= render #comments %>
#app/views/scoreboards/_comment.html.erb
<%= link_to comment.title, comment_path(comment) %>
<div class="nested">
<%= render comment.children if comment.has_children? %>
</div>

Resources