I have in my Rails 5 api project two models: Places and Beacons (Place has_many Beacons, foreign key: place_id). This API accepts JSON, such as this one:
{
"place":{
"name": "bedroom"
},
"beacon":{
"SSID": "My Wi-Fi",
"BSSID": "00:11:22:33:44:55",
"RSSI": "-55"
}
}
This JSON works just fine, with these classes:
def create
#place = Place.new(place_params)
#beacon = Beacon.new(beacon_params)
if #place.save
#beacon.place_id=#place.id
if #beacon.save
render :json => {:place => #place, :beacon => #beacon}, status: :created, location: #places
else
render json: #beacon.errors, status: :unprocessable_entity
end
else
render json: #place.errors, status: :unprocessable_entity
end
end
private
# Use callbacks to share common setup or constraints between actions.
def set_place
#place = Place.find(params[:id])
end
# Only allow a trusted parameter "white list" through.
def place_params
params.require(:place).permit(:name)
end
def beacon_params
params.require(:beacon).permit(:SSID, :BSSID, :RSSI)
end
However, I want that to pass multiple Beacons objects in the same JSON in an array (even no beacons at all). How can I save then all beacons in the parameter array and generate a response containing all of them?
I'm assuming Place has_many :beacons. If so, you can use nested attributes. This lets you assign and update a nested resource, in this case Beacons. First, in your Place model:
accepts_nested_attributes_for :beacons
Then, change your place_params method in your controller to permit the beacon's nested attributes:
def place_params
params.require(:place).permit(:name, beacons_attributes: [:id, :SSID, :BSSID, :RSSI])
end
And your json:
{
"place":{
"name": "bedroom",
"beacons_attributes": [
{
"SSID": "My Wi-Fi",
"BSSID": "00:11:22:33:44:55",
"RSSI": "-55"
}, {
//more beacons here
}
]
},
}
You can have zero, one, or more beacons, and can do the same when updating as well if you include the beacons' id's.
You also won't have to create the #beacon manually, and can just do this: #place = Place.new(place_params), and it'll automatically create the beacons and associate them to the Place.
You can read more here, if you'd like more information or clarification: http://api.rubyonrails.org/classes/ActiveRecord/NestedAttributes/ClassMethods.html
Edit: I missed where you asked how to include the beacons in the response. The quickest way is to just include it in the json rendering:
render :json => #place.to_json(:include => :beacons)
You can use Active model serializers (https://github.com/rails-api/active_model_serializers), jbuilder (https://github.com/rails/jbuilder), or more simply just override the to_json method on your model.
Related
I am trying to setup a Rails API backend for a bookstore project, and the frontend will be react. The backend has only one model for now which is the Book model. In the at the frontend I will like to filter the books based on their category. I want to setup the filtering at the backend so that when I select a dropdown at the frontend, only books that fit a particular category will be displayed. So far here is what I have at the backend:
Book model
class Book < ApplicationRecord
validates :title, :category, presence: true
scope :categorized, -> (category) { where("category LIKE ?", "#{category}" ) }
end
my book controller looks like this:
Book controller
class Api::V1::BooksController < Api::V1::ApiController
before_action :set_book, only: %i[show update destroy]
def index
#books = Book.all
render json: #books
end
def categorized
#category = params[:book]
#books = Book.categorized(#category)
render json: #books
end
def show
render json: #book
end
def create
#book = Book.new(book_params)
if #book.save
render json: #book, status: :created
else
render json: #book.errors, status: :unprocessable_entity
end
end
def update
if #book.update(book_params)
render json: #book
else
render json: #book.errors, status: :unprocessable_entity
end
end
def destroy
#book.destroy
end
private
def set_book
#book = Book.find(params[:id])
end
def book_params
params.require(:book).permit(:title, :category)
end
end
Routes.rb
Rails.application.routes.draw do
namespace :api do
namespace :v1 do
resources :books do
collection do
get :categorized
end
end
end
end
end
Sample Data
Here is a sample data I get from the API when I query for all books in the database:
[
{
"id": 1,
"title": "I Know Why the Caged Bird Sings",
"category": "Fable"
},
{
"id": 2,
"title": "His Dark Materials",
"category": "Speech"
},
{
"id": 3,
"title": "To Say Nothing of the Dog",
"category": "Fable"
},
{
"id": 4,
"title": "An Acceptable Time",
"category": "Science fiction"
},
{
"id": 5,
"title": "A Scanner Darkly",
"category": "Suspense/Thriller"
},
{
"id": 6,
"title": "The Golden Bowl",
"category": "Science fiction"
}
]
My interest is I want to see only "Science fiction" books or any other category I choose only
using scopes. The categories are not limited to what is shown in the sample data; infact they
are likely to increase. The configuration I have above in the model is not getting what I
need. When I use postman, I do not get any result. The best I get is an empty array.
scope :categorized, -> (category) { where("category LIKE ?", "#{category}" ) }
Are you sure you need this scope for only one place?
Why don’t just filter in controller?
Second thought is using Book.where(category: category) which will produce “WHERE CATEGORY = ?” instead of “WHERE category LIKE”
Third - may be you can get rid of categorized route and just use index with optional param? You can also add pagination later in index controller - where...page(page).per(per)
Fourth, there is a good practice of using instance variable accessor methods instead of variable itself:
def set_book
#book = Book.find(params[:id])
end
To
def book
#book ||= Book.find(params[:id])
end
Also find method raises exception. You might want to rescue from it on class level. You can also use book.update! And book.save! in other methods and rescue from them as well - then you won’t need those if/else branches.
I'm wanting to build a Rails API that creates nested records in the database.
I'm wanting to post data to the database for nested or say associated model with a has_ many association [see code below].
Is this the correct approach? Or is there any alternate approach for the update action? I want to record the users Address which will be in array of hashes in json format.
I have two models User and Address.
Association is User has_many Addresses.
My update action:
def update
#user = User.find(id: params[:id])
if #user.update(first_name: params[:first_name],email: params[:email],phone: params[:phone],password: Time.now.to_i, gstin: params[:gstin],addresses)
#user.addresses.each do |a|
a.update_attributes(first_name: params['first_name'],last_name: params['last_name'],city: params['city'],state: params['state'],phone: params['phone'],zip: params['zip'],address: params['address'])
end
render json: #user, status: 200
else
render json: { errors: #user.errors }, status: 422
end
end
thats not correct way to do
you should use accepts_nested_attributes_for
Class User
has_many :addresses
accepts_nested_attributes_for :addresses
end
for more details and example follow this
I figured it out and writing the answer for the people who get stuck.
As I was not using strong params and I needed only the address in json format, I parsed the address json like this:
def update
#user = User.find(params[:id])
if #user.update(first_name: params[:first_name],email: params[:email],phone: params[:phone], gstin: params[:gstin])
updated_addreses = JSON.parse(params[:addresses])
updated_addreses.each do |add|
#user.addresses.update(first_name: add['first_name'],city: add['city'],state: add['state'],phone: add['phone'],zip: add['zip'],address: add['address'])
end
render json: {user_id: #user.id.to_s}, status: 200
else
render json: { errors: #user.errors }, status: 422
end
end
I'm trying to create a RESTful API for a very simple web app I made using Ruby on Rails. Specifically, I'm trying to implement the POST /users.json to create a new user.
The "parsing the response to JSON" bit is handled by the scaffolding. The issue comes when trying to use the strong parameters method scaffolded.
I make a POST request using the Postman Chrome extension to:
# POST /users
# POST /users.json
def create
user_params[:karma] = 1
#user = User.new(user_params)
respond_to do |format|
if #user.save
format.html { redirect_to #user, notice: 'User was successfully created.' }
format.json { render :show, status: :created, location: #user }
else
format.html { render :new }
format.json { render json: #user.errors, status: :unprocessable_entity }
end
end
end
So user_params is called, and it requires a user (note that this method was generated by the scaffolding):
def user_params
params.require(:user).permit(:name, :karma, :about)
end
I realised I can work this around by simply "not requiring" the user in params:
def user_params
params.permit(:name, :karma, :about)
end
But is this safe or appropriate? Is there a more correct way?
And why is the user even required in the first place, if that's exactly what I intend to create?
params.require(:user).permit(:name, :karma, :about)
Says that the params hash MUST contain a key called user and checks that associated value only contains the named keys. Aside from this security check, it returns pretty much what params[:user]. This requires that the params hash is of the form
{
:user => {
:name => "Bob",
:about => "Professional builder",
:karma => "10"
}
}
You'll get a parameters hash like this if the field names in the form / http request are user[name], user[about], which is exactly what you'll get if you use the rails form helpers
On the other hand it sounds like the parameters hash your sending is
{
:name => "Bob",
:about => "Professional builder",
:karma => "10"
}
because in your request the field names are name, about, karma.
The problem with doing params.permit(:name, :karma, :about) is that it stops you from ever passing other parameters to the action because the parameter checker won't allow them (and if you did allow them then User.new would complain).
I have an rails app with json api. So far I can create single objects via POST request.
It's fairly simple:
def create
customer = Customer.new(customer_params)
if customer.save
render json: customer, status: 201
else
render json: customer.errors, status: 422
end
end
and:
private
def customer_params
params.require(:customer).permit(:name, :city)
end
Now I want to create multiple customers by passing an array in my http request. Like this:
{
"customer": [
{
"name": "foo",
"city": "New York"
},
{
"name": "bar",
"city": "Chicago"
}
]
}
However, I don't know how to approach this. The first issue is that my strong parameters function doesn't accept arrays.
Is there a way to use strong parameters and let me loop through the array?
I would see it as a new controller method
something like:
def multi_create
render json: customer.errors, status: 422 and return unless params[:customers]
all_created = true
customers = []
params[:customers].each do |customer_params|
customer = Customer.create(name: customer_params[:name], city: customer_params[:city])
customers << customer
all_created &&= customer.valid?
end
if all_created
render json: customers, status: 201
else
render json: customers.map(&:errors), status: 422
end
end
You also need to add the route. then you could post your json to that route with the change that the outermost key should be customers.
I would not run this code without any changes but you get the general idea. And you can refactor it to your liking.
I need to validate a field before the create method
In my _form.html.erb I have two models, one is the owner model, and the other is a model I create to have other arguments, I need to validate those arguments before getting in the create method, I can use an if, but it is not the best practice to do it.
def create
#customer = Customer.new(customer_params)
#read the city name, since this is requested by city name (string) and it shoud be "id" in the system
city = city_params()
#customer.city_id = City.find_by(name: city["name"]).id
respond_to do |format|
if #customer.save
format.html { redirect_to #customer, notice: 'Customer was successfully created.' }
format.json { render action: 'show', status: :created, location: #customer }
else
format.html { render action: 'new' }
format.json { render json: #customer.errors, status: :unprocessable_entity }
end
end
end
I need to validate the the city name, because the customer owner must have the city_id, and the _form requests the name (string), so I need to find the city but previously I need to validate the city name has a value and it exists,
How can I validate this in the model ?
If I were you, I would start out by keeping all of this logic in the controller and use a filter to find the city:
class CustomersController < ApplicationController
before_action :find_city, only: [:create, :update]
def create
#customer = Customer.new(customer_params)
#read the city name, since this is requested by city name (string) and it shoud be "id" in the system
#customer.city_id = #city.try(:id) # This returns `nil` if the city was not found
respond_to do |format|
if #customer.save
format.html { redirect_to #customer, notice: 'Customer was successfully created.' }
format.json { render action: 'show', status: :created, location: #customer }
else
format.html { render action: 'new' }
format.json { render json: #customer.errors, status: :unprocessable_entity }
end
end
end
private
def find_city
#city = City.find_by(name: params[:city][:name]) # No need for strong parameters for this
end
end
Then make sure you're validating the presence of city_id in your Customer class:
class Customer < ActiveRecord::Base
validates :city_id, presence: true
end
Later on, if you find that you need this logic to be extracted from the controller, then consider looking at creating a service object or a form object. Because this is a simple case (only 2 classes are involved), I would hold off on creating those constructs for now though. The controller layer is sufficient enough to handle this simple logic.
Why not move the logic directly into the model? I can tell you from experience that you do not want to mess your model up with tons of logic involving other model classes. Customer should not really know much about City in my opinion.
before_validate
You could use the before_validate callback in your model:
#app/models/customer.rb
Class Customer < ActiveRecord::Base
attr_accessor :city_name
before_validate :set_city
private
def set_city
city_id = City.find_by(name: city_name).id
end
end
--
Custom Validation Method
I think the bottom line is you'll be best using a custom validation method for this. You basically want to return the user to the form with an error saying "City not found" or similar; which is entirely within the remit of a custom validation method:
#app/models/customer.rb
Class Customer < ActiveRecord::Base
validate :check_city_id
private
def check_city_id
errors.add(:city_id, "City doesn't exist") unless City.try city_id
end
end
--
System
This kind of issue can be handled by simply giving the user options to select the id at input; rather than selecting by name:
#app/views/customers/new.html.erb
<%= form_for #customer do |f| %>
<%= f.select :city_id, City.all.collect {|p| [ p.name, p.id ] } %>
<% end %>
I think your method of giving the user the ability to pick a city name, and then validating in the backend is very inefficient; whilst giving the user a rigid set of options to select a buyer by city is far more robust
We have something called callbacks http://api.rubyonrails.org/classes/AbstractController/Callbacks/ClassMethods.html ..using this we can trigger our required validations in the model.
You can create your own logic of validation like example
before_create :method_name
def method_name
your logic.....example: validates :city_name ..
end