I am trying to create a web API that allows creation of FriendShip by email or phone_number.
class Api::FriendshipsController < Api::BaseController
respond_to :json
def create
friend = User.where("email = ? OR phone_number = ?", params[:emailOrPhone], params[:emailOrPhone]).first # create a friend by email or phone_number
if friend.valid? # check if the friend exists, if it does we create our new friendship
friendship = Friendship.new
friendship.user = current_user
friendship.friend = friend
if friendship.valid? # check if friendship is valid
friendship.save # if it is, we save and return a success JSON response
render json: {created: true}, status: 200
else # if it's not a valid friendship, we display a error JSON response
render json: {created: false}, status: 400
end
end
end
end
Here is my FriendShip model
class Friendship < ActiveRecord::Base
belongs_to :user
belongs_to :friend, :class_name => "User"
validates_uniqueness_of :user_id, scope: :friend_id, :message => '%{friend_id} is already a friend'
validate :check_friend_and_user # prevent user trying to add him/herself as friend.
def check_friend_and_user
errors.add(:friend, "can't be the same as user") if user == friend
end
end
Whenever the uniqueness constrain is violated, an error missing interpolation argument :friend_id in "%{friend_id} is already a friend" ({:model=>"Friendship", :attribute=>"User", :value=>2} given) with error code 500
How do I make it not throw an error but instead proceed to give back a 'fail json response' with status code 400
I want the caller of this API to know that they are trying to add someone that is already a friend. Getting back a status code 500 and a bunch of html does not seems to uniquely identify it. So I would like to throw a error in the form of JSON and status 200
What you seem to be trying to do is determine if the friend is already associated to the User via the friendship class. Which you can simplify with a has_many :friendships association on the User object.
Also, the way you're looking up by email OR phone is IMO unnecessarily ambiguous and will be problematic if you want to separately track one or the other for other purposes. Which you seem to be wanting to do since you have them broken out into separate database columns. I think you could just put two form inputs email or phone number and only pass one along to the controller. If you must have only one then you could determine what the form submits in Javascript or something.
In that case, you'd be better off sending the type of identifier with your initial data from your form, so you can look for one or the other. So your form would explicitly send the column lookup identifier, e.g. the params would be equivalent to the ruby hash
{friendship: {email: "someone#somewhere.com"}}
With that in the params then you could do what your trying with this code
# assuming you're passing via a params hash that would look like
# one or the other of the following
# {friendship: {email: "john#doe.com"}}
# {friendship: {phone_number: "123-123-1234"}}
def create
if current_user.friendships.find_or_create_by(friendship_params)
render json: {created: true}, status: 200
else # if it's not a valid friendship, we display a error JSON response
render json: {created: false}, status: 400
end
end
protected
def friendship_params
require(:friendship).permit(:email, :phone_number)
end
Related
I am trying to send one post to a create in a controller and create two objects. For some reason the contoller only will create a company. I did a similair thing in sinatra and it worked. I know that the route is correct and so is the object the post sends.
Conrtoller:
def index
stocks = current_user.stocks
render json: stocks, include: ['company']
end
def create
company = Comapny.find_or_create_by(name: params["company"])
stock = current_user.stocks.create(stock_params)
render json: stock, status: :created
end
Params:
def stock_params
params.require(:stock).permit(:name, :price_purchased_at, :number, :info, :restaurant )
end
Serializer:
class StockSerializer < ActiveModel::Serializer
attributes :id, :name, :price_purchased_at, :info, :number, :company
belongs_to :user
belongs_to :company
end
I have tried changing the serializer and the params. I have also tried leaving out the company create line to see if it will create the stock but it still won't create a stock.
You ensure in your create that a company with the expected name exists. But you do not pass the found company to the stock creation method. Therefore, the stock creation fails with Company must exist.
Just change your create method to this:
def create
company = Company.find_or_create_by(name: params['company'])
stock = current_user.stocks.create!(stock_params.merge(company_id: company.id))
render json: stock, status: :created
end
I am running into a problem when trying to handle a #create request on a model with a has_many association where one of the passed in IDs does not belong to an existing record.
Test request:
post authors_path, params: { book_ids: [-1] }
Controller method:
def create
#author= Author.create params
end
Model:
class Author
has_many :books
end
This results in an ActiveRecord::RecordNotFound error being raised.
The problem is as follows:
I am already rescuing from an ActiveRecord::RecordNotFound error and responding with 404 Record Not Found in my ApplicationController because such an error typically arises when a user is attempting to GET, PATCH, PUT, or DELETE a record that does not exist, e.g., get author_path(-1). I would prefer to avoid moving the rescue clause onto the #show, #create, etc methods because I have a lot of controllers, resulting in a lot of duplicate code.
I want to keep my record and association creations atomic and this seems to be the best way to do it, but I also want to respond with a 400 Bad Request when the situation described above occurs. What would be the best way to handle such a situation?
UPDATE
After some more research, I wrote a quick custom validation that validates a record exists for all passed in book_ids
class Author < ApplicationRecord
validate :books_exist
def books_exist
return if book_ids.blank?
Book.find book_ids
rescue ActiveRecord::RecordNotFound => e
errors.add e.message
end
end
This doesn't seem to work though as even instantiating a new Author instance without saving it to the database throws an ActiveRecord::RecordNotFound error:
> Author.new(association_ids: [-1])
Book Load (2.3ms) SELECT `books`.* FROM `books` WHERE `books`.`id` = -1
ActiveRecord::RecordNotFound: Couldn't find Book with 'id'=[-1]
from ...
The issue seems to be that ActiveRecord attempts to find a record for the passed in book_id before any validation occurs. Is there any way to rescue this error? It seems like there's not much of a workaround for this particular issue.
The two solutions that were suggest to me outside of StackOverflow are as follows:
Rescue the error in each controller action
class AuthorsController
def create
#author = Author.create(params)
render json: #author
rescue ActiveRecord::RecordNotFound => e
render json_error_message
end
end
Create a generic action in the ApplicationController
class ApplicationController
def create(model)
instance_variable_set("##(model.class.name}", model.create(params)
rescue ActiveRecord::RecordNotFound => e
render json_error_message
end
end
class AuthorsController
def create
super(Author)
end
end
In my Rails 4.0.2 application, an Account has_many Users, and each User belongs_to an Account. My Users can be created in one of two ways:
(a) Simultaneously with Account creation, by a post to Account#create. (Account#new displays a nested form which accepts attributes both for the Account and its first User.)
(b) In a post to User#create, made by a User with administrator privileges.
In both cases I'm validating the new User with validates :email, presence: true.
When validation fails in (a), I want to display the error message 'Please enter your email.'
When validation fails in (b), I want to display the error message 'Please enter the new user's email.'
In both cases I'm creating a User and using the same validation. The only difference is the controller action that initiates the User creation.
What's the best way to get my application to display two different error messages?
Make sure you are displaying flash messages on your page, and then just send the appropriate message as a flash message in your controller. Something like this:
class AccountsController < ApplicationController
def create
# code to build #account and #user, as a transaction
if #account.save
redirect_to wherever_you_want_url
else
if #user.errors.messages[:email] == ["can't be blank"]
flash.now[:notice] = "Please enter your email."
render :new
end
end
end
...
class UsersController < ApplicationController
before_filter check_privileges!, only: [:new, :create]
def create
# code to build #user
if #user.save
redirect_to wherever_you_want_url
else
if #user.errors.messages[:email] == ["can't be blank"]
flash.now[:notice] = "Please enter the new user's email."
render :new
end
end
end
Alright, after a bit of fumbling around here's my shot at it.
First, define a class variable in the User class:
class << self; attr_accessor :third_person end
Next, create a class method in the User class:
def self.third_person_helper(field, error)
return #third_person ? I18n.t("form.errors.third_person.#{field}.#{error}") : I18n.t("form.errors.first_person.#{field}.#{error}")
end
(Why a class variable and method? Because we'll be calling this from a validates statement, where I believe we've just got access to the class and not its instance. Trying to work with an instance method here just resulted in 'method not found' errors.)
Now, set up your two sets of error messages in your locale files:
en:
form:
errors:
third_person:
email:
blank: "this user's email can't be blank"
taken: "this user's email is already in use"
...
first_person:
email:
blank: "your email can't be blank"
taken: "your email is already in use"
...
Next, set up your validations like so, passing along the field and attribute you're validating:
validates :email, presence: { message: Proc.new { third_person_helper("email", "blank") } }
validates :email, presence: { message: Proc.new { third_person_helper("email", "taken") } }
...
Now that you've done that, you can switch to the third-person set of error messages just by setting User.third_person = true in your controller action, before you try and validate:
def create
# build the user here
User.third_person = true
if #user.save
# whatever you like
else
render :new
end
end
Finally, add this after_validation filter in your model, so you don't later get the third-person set of messages when you don't want them:
after_validation { User.third_person = false }
(If you want to avoid this filter, you can, but you'll have to call User.third_person = false in every controller action where you want to use the first-person set of messages.)
Whew. I like the solution I came up with because it doesn't clutter up the controllers with conditional code, but it's certainly more difficult to understand. If I had to program nicely with others I'd go the simpler route. I think it also violates Model-View-Controller best practices a bit by setting that model's class variable in the controller.
Because simpler's usually better, I'm accepting the other answer here as correct.
I have a user model and an email model. The user can create emails and receive them. A received email is realy just an email, but joined to the user on different field. My setup is below, but it doesn't work correctly. When I call received_messages on a user that created an email (and did not receive it) he also sees a message.
Is this possible at all this way?
class User < ActiveRecord::Base
has_many :mail_messages
has_many :received_messages, class_name: 'MailMessage', foreign_key: 'to_id'
end
class MailMessage < ActiveRecord::Base
belongs_to :user
emd
I did create a controller for these messages:
class ReceivedEmailsController < ApplicationController
before_filter :authenticate_user!
def index
messages = current_user.received_messages
if messages
render json: messages, each_serializer: MailMessageSerializer, status: :ok
else
render json: :nothing, status: :not_found
end
end
def show
message = current_user.received_messages.where(id: params[:id]).first
if message
render json: message, serializer: MailMessageSerializer, status: :ok
else
render json: :nothing, status: :not_found
end
end
end
The tests:
describe ReceivedEmailsController do
let!(:main_user) { Fabricate :user }
let!(:receiving_user) { Fabricate :user }
let!(:message) { Fabricate :mail_message, user_id: main_user.id, to_id: receiving_user.id }
describe 'get index' do
it 'should give not found when there are no emails received' do
main_user.confirm!
sign_in main_user
get :index
JSON.parse(response.body)['received_emails'].size.should eq 0
end
end
end
Instead of 0 I get 1 back, the same message that I get back when doing the test via the receiving_user. I want to get the message back via the receiving_user but not via the main_user.
I'm not sure whether it can be done this way, whether to use scopes, or even something else I'm not aware of.
Thanks.
One of your let! statements fabricates an email which belongs to main_user, so you get, correctly one result.
UPDATE
The above is incorrect. Try if messages.any? instead of if messages as it will always return true, since associations returns an array (empty array if there are no matching records)
I want to retrieve an entire user object via a get request. Currently, on of my methods looks like this:
def user
#user = User.find(current_user.id)
render :json => #user
end
This returns the following json:
"{\"annonymous_token\":null,\"email\":\"john1#doe.com\",\"name\":\"john1#doe.com\"}"
My user object / schema has a many-to-many relationship with another model called pictures.
has_many :picturization
has_many :pictures, :through => :picturization
How can I modify my code such that returning the user also returns the pictures the user has. If helpful, I am using devise for user authentication.
Thanks!
You can use .as_json and pass params as I show here: https://stackoverflow.com/a/11336145/308701
so you could do:
def user
#user = User.find(current_user.id)
render :json => #user.as_json(:include => :pictures)
end
Have you tried #user.to_json?