I'm using Rails 4 with strong parameters to try to find a user by a parameter called "provider_id".
The hope is that I'll be able to make a call with my API to a URL such as:
url.com/api/v1/user?provider=facebook?provider_id=12345
My routes are as follows: routes.rb
namespace :api do
namespace :v1 do
resources :users
match '/:provider/:provider_id', to: 'users#find_by_provider', via: 'get'
end
end
My Strong parameters are:
def user_params
params.require(:user).permit(:name, :age, :location, :provider, :provider_id) if params[:user]
end
My Function is:
def find_by_provider
#user = User.find(params[:provider_id])
respond_to do |format|
format.json { render json: #user }
end
end
Currently, I'm testing with:
url.com/api/v1/facebook/12345
and it is returning:
"{"provider"=>"facebook",
"provider_id"=>"12345"}"
which is good! But I now get the error: "Couldn't find User with id=12345"
Also, somewhat related: occasionally I receive an error that says "param not found: user".
Any suggestions? Thanks!
Change:
#user = User.find(params[:provider_id])
To:
#user = User.find_by(:provider_id => params[:provider_id])
find method will alyways search objects with the id column. Use the where method to search by other criterias/columns
Use:
#user = User.where(provider_id: params[:provider_id]).take
Take a look at http://guides.rubyonrails.org/active_record_querying.html if you want to learn more about the active record query interface.
This is a perfect example where to use find_by! (note the !).
#user = User.find_by!(:provider_id => params[:provider_id])
It works like find_by and returns one User. But if the user is not found it raises an ActiveRecord::RecordNotFound error. That exception is handled by Rails automatically and is turned into a 404 error page.
Related
could one advise me how to get a url like this in rails
http://www.example.com/users/5/ian
i tried the below but unsure:
route file:
devise_for :users
resources :users do
resources :socials
end
get '/users/:id/:firstname', controller: 'users', action: 'show'
users_controller.rb
def show
#user = User.find(params[:id], params[:firstname])
end
If you are trying to achieve 'friendly urls' then I suggest using this:
You don't have to create a special route:
get '/users/:id', controller: 'users', action: 'show'
Instead you have your model overwrite the to_param method:
class User
...
def to_param
"#{id}-#{firstname.try(:parameterize)}"
end
...
end
The url helper calls to_param to build the urls. If you overwrite it this way, you will receive a url like this:
http://localhost:3000/users/1-artloe
The rails find method calls .to_i on the params[:id] which, thankfully, interprets strings as number until it arrives at a character that can't become a number.
Examples:
'123abcde'.to_i # 123
'123-asdf'.to_i # 123
'asdf-123'.to_i # 0
So except for overwriting to_param, you don't have to do anything.
Try replacing this
def show
#user = User.find_by_id_and_firstname(params[:id], params[:firstname])
end
If what you are trying accomplish is "friendly urls" you would do it by:
# GET /users/1
# GET /users/joe
def show
#user = User.find_by!('id = :x OR firstname = :x', x: params[:id])
end
However you must ensure that property you are using in URLs is URL safe and unique. Usually a separate username or slug field is used.
Nothing special is needed in terms of routes.
These gems provide "friendly urls":
stringex
friendly_id
I have an edit and update methods as follows:
cmdbs_controller.rb
def edit
#evm = Evm.find(params[:id])
end
def update
#evm = Evm.find(params[:id])
if #evm.update(evm_params)
redirect_to #evm
else
render 'edit'
end
end
and routes as follows:
resources :cmdbs do
get :autocomplete_client_name, :on => :collection
collection do
get 'test'
end
end
The problem is that i get an error when submit my edit:
undefined method `evm_url' for #<CmdbsController:0x007fb33ac47d00>
and points me to the redirect_to #evm line.
BTW i am using patch and my url looks like that:
http://localhost:3000/cmdbs/1
The line redirect_to #evm translates to redirect_to evm_path(#evm.id). It uses evm_path because the class of #evm is Evm. So with that, it is expecting to have something like resources :evms in the routes. This means that you have to have another controller called EvmsController. Doing a redirect_to #evm basically redirects to the show action of EvmsController. I'm not sure if that is what you want to happen but that is what Rails is trying to do. Without any other stuff in your routes file, Rails don't know where to redirect.
When saving our Video object we get a no method vide_url error when trying to redirect to the video#watch action and view the object
Admin/Video/Controller
def create
#video = Video.create(user: User.first, title: params['title'], description: params['description'], file: params['video']['file'])
redirect_to #video
end
Video/Controller
def index
#videos = Video.page(params[:page]||1)
end
def watch
#video = Video.find_by!(id: params[:id])
end
Routes
get "video/index"
get "video/watch/:id" => 'video#watch'
namespace :admin do
resources :video
resources :playlist
end
Any idea what is going on? Is it because we are using custom routes for the videos?
Yes, it is your custom routes. redirect_to #video essentially calls url_for #video. From the docs for url_for:
Relying on named routes
Passing a record (like an Active Record) instead of a hash as the
options parameter will trigger the named route for that record. The
lookup will happen on the name of the class. So passing a Workshop
object will attempt to use the workshop_path route. If you have a
nested route, such as admin_workshop_path you’ll have to call that
explicitly (it’s impossible for url_for to guess that route).
So, because you've got a namespace around that resource you'll need to do:
redirect_to admin_video_path(#video)
or
redirect_to admin_video_url(#video)
Update
If you want to redirect to the watch action you'll need to either redirect to a hash of options including that action:
redirect_to controller: :video, action: :watch, id: #video.id
Or give your watch route a name in routes.rb:
get "video/watch/:id", to: 'video#watch', as: :watch_video
And redirect to that named route:
redirect_to watch_video_url(#video)
Please, Try the followings.
def create
#video = Video.create(user: User.first, title: params['title'], description: params['description'], file: params['video']['file'])
redirect_to admin_video_path(#video)
end
When I look at examples of Rails controllers, I usually see something like this:
class WidgetController < ActionController::Base
def new
#widget = Widget.new
end
def create
#widget = Widget.new(params[:id])
if #widget.save
redirect_to #widget
else
render 'new'
end
end
end
This works, but there's a couple problems:
Routes
If I add widgets to my routes.rb file:
Example::Application.routes.draw do
resources :widgets
end
GET /widgets/new will route to new and POST /widgets will route to create.
If the user enters incorrect information on the new widget page and submits it, their browser will display a URL with /widgets, but the new template will be rendered. If the user bookmarks the page and returns later or refreshes the page, the index action will be called instead of the new action, which isn't what the user expects. If there's no index action or if the user doesn't have permission to view it, the response will be a 404.
Duplication of code
As a contrived example, let's say I had some tricky logic in my new method:
def new
#widget = Widget.new
do_something_tricky()
end
Using the current approach, I'd duplicate that logic in new and create. I could call new from create, but then I'd have to modify new to check if #widget is defined:
def new
#widget ||= Widget.new
do_something_tricky()
end
Plus, this feels wrong because it reduces the orthogonality of the controller actions.
What to do?
So what's the Rails way of resolving this problem? Should I redirect to new instead of rendering the new template? Should I call new inside of create? Should I just live with it? Is there a better way?
I don't think this is a problem in "the rails way" and there is no builtin functionality to allow this without getting your hands dirty. What does a user expects when bookmarking a form they just submitted and had errors? Users don't know better, and they shouldn't bookmark a failed form.
I think redirecting to new_widget_path is the cleanest solution. Yet, you should keep the errors and display them on the form. For this I recommend you keep the params in session (which I expect to be smaller than a serialized Widget object).
def new
#widget = widget_from_session || Widget.new
end
def widget_from_session
Widget.new(session.delete(:widget_params)) if session[:widget_params].present?
end
private :widget_from_session
# Before the redirect
session[:widget_params] = params
The code is self explanatory, Widget.new will only be called when widget_from_session returns nil, this is when session[:widget_params] is present. Calling delete on a hash will return de deleted value and delete it from the original hash.
UPDATE Option 2
What about submitting the form using ajax? Your controller could benefit from:
respond_to :html, :json
...
def create
#widget = Widget.new params[:widget]
#widget
respond_with #widget, location: nil
end
Based on the response code (which is set by Rails: 201 Created or 422 Unprocessable Entity), you could show the errors (available in the body of the response when validations fail) or redirect the user to #widget
This is how StackOverflow does it: https://stackoverflow.com/questions/ask. They submit the form asynchronously.
In general, I think the Rails way of solving the problem would be to put the tricky method onto the model or as a helper method, so the controller stays "thin" and you don't have to make sure to add custom behavior to both #new and #create.
EDIT: For further reading, I'd recommend the "Rails AntiPatterns" book, as they go through a lot of these common design issues and give potential solutions.
you put do_something_tricky() in its own method and call it inside the create action (but only when you're rendering the new template, ie when validation fails).
As for the bookmark issue, I don't know a good way to prevent that but to modify the routes and set the create action to the new action but using POST
get '/users/new' => 'users#new'
post '/users/new' => 'users#create'
UPDATE: using resources
resources :platos, except: :create do
post '/new' => 'plates#create', on: :collection, as: :create
end
then you can use create_platos_path in your forms
You don't need to write same function in two action , use before_filter instead.
If you want to have "widget_new_url" after incorrect submission then in your form add url of new widget path something like :url => widget_new_path .
Rails takes the url from Form .
I have this problem before, so I use edit action instead.
Here is my code.
Routes:
resources :wines do
collection do
get :create_wine, as: :create_wine
end
end
Controller:
def create_wine
#wine = Wine.find_uncomplete_or_create_without_validation(current_user)
redirect_to edit_wine_path(#wine)
end
def edit
#wine = Wine.find(params[:id])
end
def update
#wine = Wine.find(params[:id])
if #wine.update_attributes(params[:wine])
redirect_to #wine, notice: "#{#wine.name} updated"
else
render :edit
end
end
Model:
def self.find_uncomplete_or_create_without_validation(user)
wine = user.wines.uncomplete.first || self.create_without_validation(user)
end
def self.create_without_validation(user)
wine = user.wines.build
wine.save(validate: false)
wine
end
View:
= simple_form_for #wine, html: { class: 'form-horizontal' } do |f|
= f.input :complete, as: :hidden, input_html: { value: 'true' }
What I did is create a new action 'create_wine' with get action.
If user request 'create_wine', it will create a new wine without validation and redirect to edit action with a update form for attributes and a hidden field for compele .
If user has create before but gave up saving the wine it will return the last uncompleted wine.
Which means whether use save it or not, the url will be the same to /wines/:id.
Not really good for RESTful design, but solve my problem. If there is any better solution please let me know.
I began coding in Rails several weeks ago, and I can't figure out why I have this error. I'm using Devise for log-ins and Formtastic for forms. The app was working correctly until I added the Acts_like_tags_on and reset the database.
The error message:
NoMethodError in UsersController#show
undefined method `username' for nil:NilClass
app/controllers/users_controller.rb:19:in `show'
Request
Parameters:
{"id"=>"sign_in"}
This is what I have in the Users Controller:
def show
#user = User.find_by_username(params[:id])
#title = #user.username
respond_to do |format|
format.html # show.html.erb
format.json { render json: #user }
end
end
Any input would be helpful. Thanks!
After editing #user = User.find_by_username(params[:id]) to:
#user = User.find_by_user(params[:id])
The error becomes:
undefined method `find_by_user' for #
The username column does exist in the User table.
#user = User.find_by_username(params[:id])
That line above may be returning nil. If you do User.find_by_username("Superman-is-awesome") and that username does not exist in your database, it's going to return nil.
Then it is trying to do:
#title = #user.username
Which is essentially:
#title = nil.username
Which of course won't work. So could be something wrong with the parameter you are passing in.
Also, make sure your User table have a column called 'username'? Make sure you've run:
rake db:migrate
As well.
If you configured the routes correctly, you should have devise routes BEFORE user resource, like this:
devise_for :users
resources :users, except: "create"
This is actually a routing problem
The problem is that devise expects you to have a route that will turn:
"/users/sign_in" into sessions#new
but your routes file is missing that route, and so the dispatcher is matching against the:
"users/:id" route which then goes to:
users#show with :id => 'sign_in'
(and hence throws an error when it tries to find a user with the id of "sign_in")
You need to read the README doc for devise (google if you don't have it locally) - especially the part that describes how to add the standard set of routes to config/routes.rb Then do whatever it says. :)
Should be right after that.
I found that the user_id was being given the value of 'users'. After commenting the following line out in my routes.rb, the id was no longer given that value.
match '/:id' => 'users#show', :as => :user
I also needed the users_controllers #show to have the following line, since my user path uses the username. removing '_by_username' caused an error on pages that called for the username:
#user = User.find_by_username(params[:id])