Rails: Validate params before they are handled in controller? - ruby-on-rails

I'm making a rails app where user can paste a soundcloud link in an input field. Then, this link is sent to the create action in my post_controller and used to get the JSON file for that song.
# app/controllers/post_controller.rb
def create
require 'open-uri'
raw_link = params[:post][:link]
raw_link.downcase.include? "soundcloud.com/"
tmp_media_json = JSON.load(open("http://soundcloud.com/oembed?format=json&url=#{raw_link}"))
if tmp_media_json['thumbnail_url']["placeholder"]
tmp_media_json['thumbnail_url'] = JSON.load(open("http://soundcloud.com/oembed?format=json&url=#{tmp_media_json['author_url']}"))['thumbnail_url']
end
media_thumbnail = tmp_media_json['thumbnail_url'].gsub('t500x500.jpg', 't80x80.jpg')
media_title = tmp_media_json['title']
media_iframe = tmp_media_json['html']
media_type = params[:post][:media_type]
#post = Post.new(link: media_iframe, title: media_title, thumbnail: media_thumbnail, media_type: media_type)
respond_to do |format|
if #post.save
format.js { render :file => "/pages/create_new_post.js.erb" }
format.html { redirect_to #post, notice: 'Post was successfully created.' }
format.json { render action: 'show', status: :created, location: #post }
else
format.html { render action: 'new' }
format.json { render json: #post.errors, status: :unprocessable_entity }
end
end
end
In the Post model, I'm trying to run validates :link, presence: true, but the problem is that it seems to be done after all the code in the create action. I want the validation to be done before all the code in the create action. (Since if there isn't a valid link, the code in the create action won't work).
How can I do this or is there a better practice?

class YourController < ApplicationController
before_filter :my_filter
before_filter :my_filter2, :except => [:index, :new, :create]
def my_filter
# your code gose here
end
def my_filter2
# your code gose here
end
........
end

You need to move the json fetching code out of the controller and into a model. Think Single Responsibility Principle(SRP): http://en.wikipedia.org/wiki/Single_responsibility_principle
Also, This question is slightly confusing because you are validating the "link" attribute which is the result of the JSON load from soundcloud, so this kind of makes your question impossible to achieve.
That being said, keep the controller lean
# controller...
def create
#post = Post.new_from_link(params[:post][:link], params[:post][:media_type])
#post.save
...render...
end
# post model...
class Post < ActiveRecord::Base
attr_accessor :raw_link, :raw_media_type
def new_from_link(raw_link, raw_media_type)
#raw_link = raw_link
#raw_media_type = raw_media_type
end
def assign_soundcloud_attributes
fetcher = SoundCloudFetcher.new(raw_link, raw_media_type).fetch
self.link = fetcher.link
self.media_type = fetcher.media_type
self.media_thumbnail = fetcher.media_thumbnail
self.media_iframe = fetcher.media_iframe
end
end
class SoundCloudFetcher
attr_accessor :link, :media_type, :media_thumbnail, :media_title, :media_iframe
def self.initialize(raw_link, raw_media_type)
#raw_link = raw_link
#raw_media_type = raw_media_type
end
def fetch
...go get and set the data...
return self
end
end
So the code above is not complete. Its missing the actual call to #assign_soundcloud_attributes. This design lets you move around where you want to do the call. You can place the call in #new_from_link, or you can place it in a before_validation or before_create callback, depending on your needs.
The question to clear this up is are you intending to validate the raw_link thats passed in, or do you want to validate the link that comes back from the soundcloud api call.
If you are validating the raw link, move the call to #assign_soundcloud_attributes to a before_create callback.
If you are validating the actual link attribute that is retrieved from the SoundCloud api call, then put it in the #new_from_link or #before_validation callback.

You can use a before_filter with rails_parms gem.
See https://github.com/nicolasblanco/rails_param
It works like this:
param! :q, String, required: true
param! :categories, Array
param! :sort, String, default: "title"
param! :order, String, in: %w(asc desc), transform: :downcase, default: "asc"
param! :price, String, format: /[<\=>]\s*\$\d+/

Related

Rails default parems for id set to new?

I am getting a 404 error on my RoR app and i found that it was because one of the method in the controller, which should only triggers when the record is not new, triggered when the record is new.
I do by that checking if the id of that record nil in my controller.
before_action :create_record, if: proc { not params[:id].nil? }
I was confused by it was triggered so i went head and checked my front-end network, which show following:
Request
Parameters:
{"format"=>"json", "id"=>"new"} <----Set to new by default
My completely controller looks like this:
class Api::MyController < ApplicationController
before_action :create_recotrd, if: proc { not params[:id].nil? }
def show
end
def index
#my_model = MyModel.all
end
def create
#my_model = MyModel.new(my_model_params)
respond_to do |format|
if #my_model.save
format.json { render json: #my_model, status: :created}
else
format.json { render json: #my_model.errors, status: :unprocessable_entity}
end
end
end
def update
#my_model = MyModel.update
end
private
def create_record
#my_model = MyModel.find(params[:id])
end
def my_model_params
params.require(:my_model).permit(
:city,
:state,
:county,
:zip,
:telephone,
:email,
)
end
end
I cant seem to find out why the id in the parameters is set to "new" instead of "nil".
I tried in the console by doing MyModel.new, the default id was nil, but then when i do the GET request, the id was set to "new"
This is a really weird approach to set a new record. I think the problem lies in your routes. You are probably trying to access yoursite.com/your_model/new and your routes are configured to look for
get "your_model/:id" => "your_controller#show"
You are probably missing
get "your_model/new" => "your_controller#new"
So when you try to visit your_model/new the routes map the "new" as the :id param in your url.
I don't see a new action in your controller as well. You should read up on basic resource set up for rails here: http://guides.rubyonrails.org/routing.html.

In Rails How to display errors in my comment form after I submit it?

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

Rails custom validate - uniqueness of a property across models

I'm stuck at defining a custom validation method that's purpose is to verify uniqueness of a property across two models
I realize this is bad code, but i wanted to get the test passing before refactor
here is a model with the custom validation to check another model property, error undefined local variable or method `params' (be gentle I'm still trying to figure out RoR)
class Widget < ActiveRecord::Base
include Slugable
validates :name, presence: true
validate :uniqueness_of_a_slug_across_models
def uniqueness_of_a_slug_across_models
#sprocket = Sprocket.where(slug: params[:widget_slug]).first
if #sprocket.present?
errors.add(:uniqueness_of_a_slug_across_models, "can't be shared slug")
end
end
end
You don't have access to params in a model. It belongs to controller and view. What you could do is to call custom method in widgets controller (instead of regular save) in order to pass params to a model:
class WidgetsController < ActionController::Base
def create
#widget = Widget.new(widget_params)
if #widget.save_with_slug_validation(params)
redirect_to widgets_path
else
render :new
end
end
end
and define it:
class Widget < ActiveRecord::Base
# ...
def save_with_slug_validation(params)
sprocket = Sprocket.find_by(slug: params[:widget_slug])
if sprocket
errors.add(:uniqueness_of_a_slug_across_models, "can't be shared slug")
end
save
end
end
I didn't test it but it should work.
P.S. Rails 4 style is used.
UPD
I should have tested it, sorry. Please use another approach.
Widgets controller:
# POST /widgets
# POST /widgets.json
def create
#widget = widget.new(widget_params)
#widget.has_sprocket! if Sprocket.find_by(slug: params[:widget_slug])
respond_to do |format|
if #widget.save
format.html { redirect_to [:admin, #widget], notice: 'widget was successfully created.' }
format.json { render action: 'show', status: :created, location: #widget }
else
format.html { render action: 'new' }
format.json { render json: #widget.errors, status: :unprocessable_entity }
end
end
end
Widget model:
class Widget < ActiveRecord::Base
include Slugable
validates :name, presence: true
validate :uniqueness_of_a_slug_across_models, if: 'has_sprocket?'
def uniqueness_of_a_slug_across_models
errors.add(:uniqueness_of_a_slug_across_models, "can't be shared slug")
end
def has_sprocket!
#has_sprocket = true
end
def has_sprocket?
!!#has_sprocket
end
end
It would be better to move has_sprocket! and has_sprocket? methods and maybe validation itself to Slugable concern.

Rails: trouble sorting array from view

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.

Cannot access attr_accessor defined variables

I am using Thinking Sphinx to run searches and I get the appropriate ActiveRecord Models fine. The problem is, I want to create an appropriate link path and text on each model, then send the info to the browser in the form of JSON, via AJAX. I am using the following to build those link attributes:
In the controller:
class FindController < ApplicationController
def tag_results
#results = ThinkingSphinx.search(params[:terms])
#results.each do |result|
result.build_ajax_response
end
respond_to do |format|
format.html
format.json { render :json => #results }
end
end
end
In the model:
class TaggedItem < ActiveRecord::Base
attr_accessible :name
attr_accessor :search_link, :search_text
def build_ajax_response
self.search_link = Rails.application.routes.url_helpers.tagged_item_path(self.id)
self.search_text = self.name
end
end
The resulting json object doesn't have either of the search_* attributes listed, much less have a value for them. I've tried using #search_link as well as just search_link in the build_ajax_response method.
Am I doing this wrong? Could there be something else interfering?
Rails' default to_json doesn't know about those extra non active record attributes you've added. The easiest possible thing is probably to specify them as extra methods to include:
format.json { render :json => #results.to_json(:methods => [:search_link, :search_text]) }

Resources