So I'm trying to let the user sort an array of recipes from a link in my view:
<%= link_to "Score", recipes_sort_path, :order => 'score' %>
I send the parameter "score" to my controller method "sort", which looks like this:
def sort
if (params[:order] == 'score')
#recipes.sort_by(&:score)
end
respond_to do |format|
format.html { redirect_to recipes_path }
format.json { render json: #recipe }
end
end
It redirects to the following index method:
def index
# If recipes already present, skip following
if (!#recipes)
if (params[:search] || params[:tag])
#recipes = Recipe.search(params[:search], params[:tag])
else
#recipes = Recipe.all
end
end
respond_to do |format|
format.html
format.json { render json: #recipe }
end
end
The idea was to be redirected to the index view with the sorted list and just render the view.
I get no errors, but when I click the link, the page reloads but nothing happens.
The Recipe class looks like this:
class Recipe < ActiveRecord::Base
attr_accessible :instructions, :name, :slug, :score, :upvotes, :downvotes, :comments, :image
has_and_belongs_to_many :ingredients
has_and_belongs_to_many :tags
has_many :comments
belongs_to :user
delegate :name, :to => :user, :prefix => :user, :allow_nil => true
mount_uploader :image, ImageUploader
validates :name, :presence => true
def score
score = (self.upvotes - self.downvotes)
end
end
What am I doing wrong here?
There's a third option (the first 2 is from ckruse's answer). You can render the index template from the sort action
def sort
if (params[:order] == 'score')
#recipes.sort_by!(&:score)
end
respond_to do |format|
format.html { render :index }
format.json { render json: #recipe }
end
end
This will use the index template while using #recipes in the sort action. You also save one request because you're not redirecting.
One more thing I'd like to comment on is the link. It should be
<%= link_to "Score", recipes_sort_path(:order => 'score') %>
UPDATE: fetching #recipes. As much as possible, I want sql to do the sorting so that's what I'm going to do here.
def sort
#recipes = Recipe
if params[:order] == 'score'
#recipes = #recipes.order('upvotes - downvotes')
end
respond_to do |format|
format.html { render :index }
format.json { render json: #recipe }
end
end
First of all: sort_by is not „destructive,” it returns a new array. You may want to use sort_by! or save the return value of sort_by into #recipes.
Second: you do not render anything in your sort action at all. If you posted all code, even #recipes is empty. You can do two things:
Retreive the data in your sort method as you did in your index method and then call render :index
Sort in your index method and do not use a sort method at all. You can route multiple URIs to the same action.
Related
I've been worrying this bug for too many days and I'm not sure what I'm missing.
I have a parent model named 'product' and a child of it 'product_attachment'.
Validation within child model is successful on create to disallow a blank image field via /product_attachments#new
However, when using its parent form /product#new (files below) I'm expecting it to validate to successfully fail without an image. However, Activerecord is ignoring the error, and my controller is tryign to save as a result if the validation passing then complaining that my product_attachments is null.
I'm currently in the understanding that using the 'validates_associated' would validate the child model as part of the Parent's process but this has not been working.
Instead of passing a happy failed validation mid-form to allow user to take action, we leave form and the controller tries to process the create method which fails due to no attachment.
Since I should always have an attachment have been trying to fix this validation to no avail.
Any help appreciated, I've include similar code samples before for your feedback.
I'm fairly new to rails so I'm hoping I'm mis-using a key syntax or context.
Also curious what cause is and what best way was to troubleshoot as I'm still developing good debug practices.
product.rb
class Product < ActiveRecord::Base
has_many :product_attachments
validates_presence_of :title, :message => "You must provide a name for this product."
accepts_nested_attributes_for :product_attachments, allow_destroy: true#,
validates_associated :product_attachments
end
product_attachment.rb (carrierwave to handle uploading used here, seems to work fine)
class ProductAttachment < ActiveRecord::Base
belongs_to :product
mount_uploader :image, ImageUploader
validates_presence_of :image, :message => "You must upload an image to go with this item."
end
products_controller.rb
class ProductsController < ApplicationController
before_action :set_product, only: [:show, :edit, :update, :destroy]
def index
#products = Product.all
end
def show
#product_attachments = #product.product_attachments.all
end
def new
#product = Product.new
#product_attachment = #product.product_attachments.build
end
def edit
end
def create
#product = Product.new(product_params)
respond_to do |format|
if #product.save
params[:product_attachments]['image'].each do |a|
#product_attachment = #product.product_attachments.create!(:image => a)
end
format.html { redirect_to #product, notice: 'Product was successfully created.' }
else #- when would this fire?
format.html { render :new }
end
end
end
def update
respond_to do |format|
if #product.update(product_params)
params[:product_attachments]['image'].each do |a|
#product_attachment = #product.product_attachments.create!(:image => a, :post_id => #post.id)
end
format.html { redirect_to #product, notice: 'Product was successfully updated.' }
else #- when would this fire?
format.html { render action: 'new' }
end
end
end
def destroy
#product.destroy
respond_to do |format|
format.html { redirect_to #product, notice: 'Product was successfully destroyed.' }
end
end
private
def set_product
#product = Product.find(params[:id])
end
# we pass the _destroy so the above model has the access to delete
def product_params
params.require(:product).permit(:id, :title, :price, :barcode, :description, product_attachment_attributes: [:id, :product_id, :image, :filename, :image_cache, :_destroy])
end
end
product_attachments_controller.rb
class ProductAttachmentsController < ApplicationController
before_action :set_product_attachment, only: [:show, :edit, :update, :destroy]
def index
#product_attachments = ProductAttachment.all
end
def show
end
def new
#product_attachment = ProductAttachment.new
end
def edit
end
def create
#product_attachment = ProductAttachment.new(product_attachment_params)
respond_to do |format|
if #product_attachment.save
#product_attachment.image = params[:image]
format.html { redirect_to #product_attachment, notice: 'Product attachment was successfully created.' }
else
format.html { render :new }
end
end
end
def update
respond_to do |format|
if #product_attachment.update(product_attachment_params)
#product_attachment.image = params[:image]
format.html { redirect_to #product_attachment.product, notice: 'Product attachment was successfully updated.' }
else
format.html { render :edit }
end
end
end
def destroy
#product_attachment.destroy
respond_to do |format|
format.html { redirect_to product_attachments_url, notice: 'Product attachment was successfully destroyed.' }
end
end
private
# Use callbacks to share common setup or constraints between actions.
def set_product_attachment
#product_attachment = ProductAttachment.find(params[:id])
end
def product_attachment_params
params.require(:product_attachment).permit(:id, :product_id, :image, :image_cache)
end
end
_form.html.slim (using simple_form + slim + cocoon here...)
= simple_form_for #product do |f|
- if #product.errors.any?
#error_explanation
h2
= pluralize(#product.errors.count, "error")
| prohibited this product from being saved:
ul
- #product.errors.each do |attribute, message|
- if message.is_a?(String)
li= message
= f.input :title
= f.input :price, required: true
= f.input :barcode
= f.input :description
h3 attach product images
#product_attachment
= f.simple_fields_for :product_attachments do |product_attachment|
= render 'product_attachment_fields', f: product_attachment
.links
= link_to_add_association 'add product attachment', f, :product_attachments
= f.submit
_product_attachment_fields.html.slim
Noted I needed to name my file field this way for my controller to use files correctly, but unsure why still.
.nested-fields
= f.file_field :image , :multiple => true , name: "product_attachments[image][]"
= link_to_remove_association "remove", f
Let me know if I can provide anything else.
Thank you for your time to read/reply.
Edit1: My current method to debug I'm working through as of writing this is to strip out code chunks and test functionality by through browser. I've read I should be more familiar with rails console but have not got there yet.
Edit2:
undefined method `[]' for nil:NilClass
params[:product_attachments]['image'].each do |a|
app/controllers/products_controller.rb:47:in `block in create'
app/controllers/products_controller.rb:44:in `create'
Request
Parameters:
{"utf8"=>"✓",
"authenticity_token"=>"{mytoken}",
"product"=>{"title"=>"pleasefailwell",
"commit"=>"Create Product"}
Edit3: Reworded original section for clarity
Seeing your latest edit, I figured that was happening, which means product_attachments is nil. Which is correct, the param should be product_attachments_attributes.
This leads to another problem in your product_params method.
You have product_attachment_attributes, the the base form "field" should be pluralised: product_attachments_attributes
In short:
Remove the product attachment loops from your controller. Fix the strong params method, it should work after that.
Edit:
Also remove the name attribute from the file_field That isn't helping.
I have a very straight-forward task to fulfil --- just to be able to write comments under posts and if the comments fail validation display error messages on the page.
My comment model uses a gem called Acts_as_commentable_with_threading, which creates a comment model after I installed.
On my post page, the logic goes like this:
Posts#show => display post and a form to enter comments => after the comment is entered, redisplay the Post#show page which has the new comment if it passes validation, otherwise display the error messages above the form.
However with my current code I can't display error messages if the comment validation fails. I think it is because when I redisplay the page it builds a new comment so the old one was erased. But I don't know how to make it work.
My codes are like this:
Comment.rb:
class Comment < ActiveRecord::Base
include Humanizer
require_human_on :create
acts_as_nested_set :scope => [:commentable_id, :commentable_type]
validates :body, :presence => true
validates :first_name, :presence => true
validates :last_name, :presence => true
# NOTE: install the acts_as_votable plugin if you
# want user to vote on the quality of comments.
#acts_as_votable
belongs_to :commentable, :polymorphic => true
# NOTE: Comments belong to a user
belongs_to :user
# Helper class method that allows you to build a comment
# by passing a commentable object, a user (could be nil), and comment text
# example in readme
def self.build_from(obj, user_id, comment, first_name, last_name)
new \
:commentable => obj,
:body => comment,
:user_id => user_id,
:first_name => first_name,
:last_name => last_name
end
end
PostController.rb:
class PostsController < ApplicationController
before_action :authenticate_user!, except: [:index, :show]
def show
#post = Post.friendly.find(params[:id])
#new_comment = Comment.build_from(#post, nil, "", "", "")
end
end
CommentsController:
class CommentsController < ApplicationController
def create
#comment = build_comment(comment_params)
respond_to do |format|
if #comment.save
make_child_comment
format.html
format.json { redirect_to(:back, :notice => 'Comment was successfully added.')}
else
format.html
format.json { redirect_to(:back, :flash => {:error => #comment.errors}) }
end
end
end
private
def comment_params
params.require(:comment).permit(:user, :first_name, :last_name, :body, :commentable_id, :commentable_type, :comment_id,
:humanizer_answer, :humanizer_question_id)
end
def commentable_type
comment_params[:commentable_type]
end
def commentable_id
comment_params[:commentable_id]
end
def comment_id
comment_params[:comment_id]
end
def body
comment_params[:body]
end
def make_child_comment
return "" if comment_id.blank?
parent_comment = Comment.find comment_id
#comment.move_to_child_of(parent_comment)
end
def build_comment(comment_params)
if current_user.nil?
user_id = nil
first_name = comment_params[:first_name]
last_name = comment_params[:last_name]
else
user_id = current_user.id
first_name = current_user.first_name
last_name = current_user.last_name
end
commentable = commentable_type.constantize.find(commentable_id)
Comment.build_from(commentable, user_id, comment_params[:body],
first_name, last_name)
end
end
comments/form: (this is on the Posts#show page)
<%= form_for #new_comment do |f| %>
<% if #new_comment.errors.any? %>
<div id="errors">
<h2><%= pluralize(#new_comment.errors.count, "error") %> encountered, please check your input.</h2>
<ul>
<% #new_comment.errors.full_messages.each do |msg| %>
<li><%= msg %></li>
<% end %>
</ul>
</div>
<% end %>
<% end %>
I would instead use nested routes to create a more restful and less tangled setup:
concerns :commentable do
resources :comments, only: [:create]
end
resources :posts, concerns: :commentable
This will give you a route POST /posts/1/comments to create a comment.
In your controller the first thing you want to do is figure out what the parent of the comment is:
class CommentsController < ApplicationController
before_action :set_commentable
private
def set_commentable
if params[:post_id]
#commentable = Post.find(params[:post_id])
end
end
end
This means that we no longer need to pass the commentable as form parameters. Its also eliminates this unsafe construct:
commentable = commentable_type.constantize.find(commentable_id)
Where a malicous user could potentially pass any class name as commentable_type and you would let them find it in the DB... Never trust user input to the point where you use it to execute any kind of code!
With that we can start building our create action:
class CommentsController < ApplicationController
before_action :set_commentable
def create
#comment = #commentable.comments.new(comment_params) do |comment|
if current_user
comment.user = current_user
comment.first_name = current_user.first_name
comment.last_name = current_user.last_name
end
end
if #comment.save
respond_to do |format|
format.json { head :created, location: #comment }
format.html { redirect_to #commentable, success: 'Comment created' }
end
else
respond_to do |format|
format.html { render :new }
format.json { render json: #comment.errors, status: 422 }
end
end
end
private
# ...
def comment_params
params.require(:comment).permit(:first_name, :last_name, :body, :humanizer_answer, :humanizer_question_id)
end
end
In Rails when the user submits a form you do not redirect the user back to the form - instead you re-render the form and send it as a response.
While you could have your CommentsController render the show view of whatever the commentable is it will be quite brittle and may not even provide a good user experience since the user will see the top of the post they where commenting. Instead we would render app/views/comments/new.html.erb which should just contain the form.
Also pay attention to how we are responding. You should generally avoid using redirect_to :back since it relies on the client sending the HTTP_REFERRER header with the request. Many clients do not send this!
Instead use redirect_to #commentable or whatever resource you are creating.
In your original code you have totally mixed up JSON and HTML responses.
When responding with JSON you do not redirect or send flash messages.
If a JSON POST request is successful you would either:
Respond with HTTP 201 - CREATED and a location header which contains the url to the newly created resource. This is preferred when using SPA's like Ember or Angular.
Respond with HTTP 200 - OK and the resource as JSON in the response body. This is often done in legacy API's.
If it fails do to validations you should respond with 422 - Unprocessable Entity - usually the errors are rendered as JSON in the response body as well.
Added.
You can scrap your Comment.build_from method as well which does you no good at all and is very idiosyncratic Ruby.
class PostsController < ApplicationController
before_action :authenticate_user!, except: [:index, :show]
def show
#post = Post.friendly.find(params[:id])
#new_comment = #post.comments.new
end
end
Don't use line contiuation (\) syntax like that - use parens.
Don't:
new \
:commentable => obj,
:body => comment,
:user_id => user_id,
:first_name => first_name,
:last_name => last_name
Do:
new(
foo: a,
bar: b
)
Added 2
When using form_for with nested resources you pass it like this:
<%= form_for([commentable, comment]) do |f| %>
<% end %>
This will create the correct url for the action attribute and bind the form to the comment object. This uses locals to make it resuable so you would render the partial like so:
I'm assuming your form_for submits a POST request which triggers the HTML format in CommentsController#create:
def create
#comment = build_comment(comment_params)
respond_to do |format|
if #comment.save
make_child_comment
format.html
format.json { redirect_to(:back, :notice => 'Comment was successfully added.')}
else
format.html
format.json { redirect_to(:back, :flash => {:error => #comment.errors}) }
end
end
end
So, if #comment.save fails, and this is an HTML request, the #create method renders create.html. I think you want to render Posts#show instead.
Keep in mind that if validations fail on an object (Either by calling save/create, or validate/valid?), the #comment object will be populated with errors. In other words calling #comment.errors returns the relevant errors if validation fails. This is how your form is able to display the errors in #new_comment.errors.
For consistency, you'll need to rename #new_comment as #comment in the posts#show action, otherwise you'll get a NoMethodError on Nil::NilClass.
TL;DR: You're not rendering your form again with your failed #comment object if creation of that comment fails. Rename to #comment in posts, and render controller: :posts, action: :show if #comment.save fails from CommentsController#create
I have figured out the answer myself with the help of others here.
The reason is that I messed up with the JSON format and html format (typical noobie error)
To be able to display the errors using the code I need to change two places ( and change #comment to #new_comment as per #Anthony's advice).
1.
routes.rb:
resources :comments, defaults: { format: 'html' } # I set it as 'json' before
2.
CommentsController.rb:
def create
#new_comment = build_comment(comment_params)
respond_to do |format|
if #new_comment.save
make_child_comment
format.html { redirect_to(:back, :notice => 'Comment was successfully added.') }
else
commentable = commentable_type.constantize.find(commentable_id)
format.html { render template: 'posts/show', locals: {:#post => commentable} }
format.json { render json: #new_comment.errors }
end
end
end
I have the following error
undefined method `[]' for nil:NilClass
app/controllers/events_controller.rb:60:in `create'
I am not sure what they mean by nil in this case. Here my controller and line 60 is where the arrow is
def create
#event = current_customer.events.build(params[:event])
#location = #event.locations.build(params[:location])
--->#location.longitude = params[:location][:longitude]
#location.latitude = params[:location][:latitude]
respond_to do |format|
if #location.save
if #event.save
format.html { redirect_to #event, notice: 'Event was successfully created.' }
format.json { render json: #event, status: :created, location: #event }
else
format.html { render action: "new" }
format.json { render json: #event.errors, status: :unprocessable_entity }
end
end
end
end
I have two models an event and a locations, I am create the two events at the same time and events has many locations. The longitude are attr_accesor longitude and latitude. hidden field type.
params[:location] is very likely to be nil. Additionally, you should consider using nested model form to have cleaner code. Refer to the RailsCast on Nested Model Forms and the docs for fields_for.
Theoretically, your model classes should look something like this:
class Customer < ActiveRecord::Base
...
has_many :events
accepts_nested_attributes_for :events
...
end
class Event < ActiveRecord::Base
...
has_one :location
accepts_nested_attributes_for :location
...
end
class Location < ActiveRecord::Base
...
belongs_to :event
...
end
your controller like this:
class EventsController < ApplicationController
def new
current_customer.events.build({}, {}) # instantiate 2 empty events
end
def create
current_customer.events.build(params[:event])
if current_customer.save # should save all events and their associated location
...
end
end
end
and your view like this:
<%= form_for #customer do |f| %>
...
<%= f.fields_for :events do |e| %>
...
<%= e.fields_for :location, (e.build_location || e.location) do |l| %>
<%= l.hidden_field :longitude %>
<%= l.hidden_field :latitude %>
<% end %>
...
<% end %>
...
<% end %>
Either params is nil, or, more likely, params[:location] is nil.
Imagine:
a = [[1,2], [3,4], [5,6], nil, [7,8]]
a[0][0]
=> 1
a[3][0]
undefined method `[]' for nil:NilClass
Because fourth element is nil, so we can't use the [] method on it..
The conclusion is that params[:location] is nil, such that when you're trying to access an element of what you think is an array, you get a method error, because NilClass doesn't have the [] method (whereas an Array does)
Write to your console:
logger.debug params[:location].class
logger.debug params[:location].inspect
I suspect the data coming is isn't what you expect it to be (i.e. [:longitude] is not part of the hash params[:location]).
The functionality I'm trying to build allows Users to Visit a Restaurant.
I have Users, Locations, and Restaurants models.
Locations have many Restaurants.
I've created a Visits model with user_id and restaurant_id attributes, and a visits_controller with create and destroy methods.
Thing is, I can't create an actual Visit record. Any thoughts on how I can accomplish this? Or am I going about it the wrong way.
Routing Error
No route matches {:controller=>"restaurants", :location_id=>nil}
Code:
Routes:
location_restaurant_visits POST /locations/:location_id/restaurants/:restaurant_id/visits(.:format) visits#create
location_restaurant_visit DELETE /locations/:location_id/restaurants/:restaurant_id/visits/:id(.:format) visits#destroy
Model:
class Visit < ActiveRecord::Base
attr_accessible :restaurant_id, :user_id
belongs_to :user
belongs_to :restaurant
end
View:
<% #restaurants.each do |restaurant| %>
<%= link_to 'Visit', location_restaurant_visits_path(current_user.id, restaurant.id), method: :create %>
<% #visit = Visit.find_by_user_id_and_restaurant_id(current_user.id, restaurant.id) %>
<%= #visit != nil ? "true" : "false" %>
<% end %>
Controller:
class VisitsController < ApplicationController
before_filter :find_restaurant
before_filter :find_user
def create
#visit = Visit.create(params[:user_id => #user.id, :restaurant_id => #restaurant.id])
respond_to do |format|
if #visit.save
format.html { redirect_to location_restaurants_path(#location), notice: 'Visit created.' }
format.json { render json: #visit, status: :created, location: #visit }
else
format.html { render action: "new" }
format.json { render json: #visit.errors, status: :unprocessable_entity }
end
end
end
def destroy
#visit = Visit.find(params[:user_id => #user.id, :restaurant_id => #restaurant.id])
#restaurant.destroy
respond_to do |format|
format.html { redirect_to location_restaurants_path(#restaurant.location_id), notice: 'Unvisited.' }
format.json { head :no_content }
end
end
private
def find_restaurant
#restaurant = Restaurant.find(params[:restaurant_id])
end
def find_user
#user = current_user
end
end
I see a lot of problems here. The first is this line of code in your VisitController's create action (and identical line in your destroy action):
#visit = Visit.create(params[:user_id => #user.id, :restaurant_id => #restaurant.id])
params is a hash, so you should be passing it a key (if anything), not a bunch of key => value bindings. What you probably meant was:
#visit = Visit.create(:user_id => #user.id, :restaurant_id => #restaurant.id)
Note that you initialize #user and #restaurant in before filter methods, so you don't need to access params here.
This line of code is still a bit strange, though, because you are creating a record and then a few lines later you are saving it (if #visit.save). This is redundant: Visit.create initiates and saves the record, so saving it afterwards is pretty much meaningless. What you probably want to do is first initiate a new Visit with Visit.new, then save that:
def create
#visit = Visit.new(:user_id => #user.id, :restaurant_id => #restaurant.id)
respond_to do |format|
if #visit.save
...
The next thing I notice is that you have not initiated a #location in your create action, but you then reference it here:
format.html { redirect_to location_restaurants_path(#location), notice: 'Visit created.' }
Since you will need the location for every restaurant route (since restaurant is a nested resource), you might as well create a method and before_filter for it, like you have with find_restaurant:
before_filter :find_location
...
def find_location
#location = Location.find(params[:location_id])
end
The next problem is that in your view your location_restaurant_path is passed the id of current_user and of restaurant. There are two problems here. First of all the first argument should be a location, not a user (matching the order in location_restaurant_path). The next problem is that for the _path methods, you have to pass the actual object, not the object's id. Finally, you have method: :create, but the method here is referring to the HTTP method, so what you want is method: :post:
link_to 'Visit', location_restaurant_visits_path(#location, restaurant.id), method: :post
You'll have to add a find_location before filter to your RestaurantController to make #location available in the view here.
There may be other problems, but these are some things to start with.
location_id is nil and the path definition doesn't say (/:location_id) forcing a non-nil value there in order to route to that path; create a new route without location_id if you can derive it from a child's attribute (i.e. a restaurant_id refers to a Restaurant which already knows its own location_id).
I want to display the related products to a certain need, just the picture and the name nothing more depending on the categorie
here is mu controller
class RelatedneedsController < ApplicationController
def index
#relatedneeds = RelatedNeed.all
respond_to do |format|
format.html # index.html.erb
format.json { render json: #relatedneeds }
end
end
def show
s1 = '#need.category.name'
s2 = '#relatedneed.category.name'
if s1.eql?(s2)
#relatedneed = relatedneed.find(params[:id])
respond_to do |format|
format.html # show.html.erb
format.json { render json: #need }
end
end
end
def get_name
#relatedneed.name
end
end
and this my model
class Relatedneed
include Mongoid::Document
include Mongoid::Paperclip
mapping do
indexes :name
end
field :name, type: String
belongs_to :category
belongs_to :user
end
and this is show.haml file
%h1
%b= #need.name
#container{:style => "width:1000px"}
#desc{:style => "height:400px;width:400px;float:left;"}
=image_tag #relatedneed.photo.url(:normal)
this is my index.haml file
%h1= #relatedneed.get_name
#container{:style => "width:1000px"}
#desc{:style => "background-color:#EEEEEE;height:400px;width:400px;float:left;"}
= link_to "Check Need", new_need_path
I don't know if their is something missing and i get this error
NoMethodError in RelatedneedsController#index
undefined method `key?' for nil:NilClass
Your index.haml has #relatedneed.get_name but you have not set #relatedneed in your controller, only #relatedneeds. Is it as simple as that?
Also, your show method in the controller makes no sense to me. You have put your instance variables inside strings! You need to set your instance variables with a database query via the model first. Your get_name method looks like it belongs in a model as well rather than a controller.