HABTM relationship with rails API does not work - ruby-on-rails

I am new in Ruby on Rails. I am making a Rails API using Rails 5.1, active record serializer, doorkeeper and devise gem.
I have an Order table and it has many products. The relation between order and product is many-to-many.
Order model:
class Order < ApplicationRecord
validates_presence_of :brute, :net
has_and_belongs_to_many :products
end
Product model:
class Product < ApplicationRecord
belongs_to :category
validates_presence_of :name, :price
validates_uniqueness_of :name
has_and_belongs_to_many :orders
end
I have a join table named orders_products.
Order serializer:
class OrderSerializer < ActiveModel::Serializer
attributes :id, :discount, :brute, :net, :payed, :payed_at, :products
def products
object.products.map do |product|
ProductSerializer.new(product, scope: scope, root: false, event: object)
end
end
end
Product serializer:
class ProductSerializer < ActiveModel::Serializer
attributes :id, :name, :price, :description
has_one :category
end
Order controller:
module Api
class OrdersController < ApiController
before_action :set_order, only: [:show, :update, :destroy]
# GET /api/orders
def index
#orders = Order.all
render json: #orders
end
# GET /api/orders/1
def show
render json: #order
end
# POST /api/orders
def create
#order = Order.new(order_params)
if #order.save
render json: #order, status: :created, location: api_order_url(#order)
else
render json: #order.errors, status: :unprocessable_entity
end
end
# PATCH/PUT /api/orders/1
def update
if #order.present?
if #order.update(order_params)
render json: #order
else
render json: #order.errors, status: :unprocessable_entity
end
end
end
# DELETE /api/orders/1
def destroy
#order.destroy if #order.present?
end
private
# Use callbacks to share common setup or constraints between actions.
def set_order
#order = Order.find(params[:id])
rescue ActiveRecord::RecordNotFound
Rails.logger.error{ 'Order record is not found' }
nil
end
# Only allow a trusted parameter "white list" through.
def order_params
params.require(:order).permit(:discount, :brute, :net, :payed, :payed_at, product_ids: [])
end
end
end
When I post some order json data from API generator app like Postman/Insomnia, Order is being saved in orders table but no data saved in orders_products join table.
My request(POST http://localhost:3000/api/orders) of order json:
{
"discount": 110,
"brute": 100,
"net": 200,
"payed": null,
"payed_at": null,
"product_ids": [3]
}
I try to find the solution but I failed.

Finally I have solved in your problem.Just add an attribute in your model.
Order Model:
class Order < ApplicationRecord
attribute :product_ids
validates_presence_of :brute, :net
has_and_belongs_to_many :products
end
Order Serializer:
class OrderSerializer < ActiveModel::Serializer
attributes :id, :discount, :brute, :net, :payed, :payed_at
has_many :products
end
And create method in your order api:
# POST /api/orders
def create
#order = Order.new(order_params)
if #order.save
# Find products
#products = Product.where(id: order_params[:product_ids])
# Create join table records
#products.each { |product| product.orders << #order }
render json: #order, status: :created, location: api_order_url(#order)
else
render json: #order.errors, status: :unprocessable_entity
end
end
I have tested in locally and it works! Happy Programming :)

As far as I know, Rails doesn't automatically handle creating the join records when given a list of ids. Therefore when you're calling #order = Order.new(order_params) and expecting it to know how to handle product_ids: [3], it's just ignoring it.
If you modify your create endpoint with the below, you should see the join records being created.
# POST /api/orders
def create
#order = Order.new(order_params)
if #order.save
# Find products
#products = Product.where(id: order_params[:product_ids])
# Create join table records
#products.each { |product| product.orders << order }
render json: #order, status: :created, location: api_order_url(#order)
else
render json: #order.errors, status: :unprocessable_entity
end
end
This is just one possible solution that doesn't do any error checking. Depending how secure and robust your application needs to be you may need to create a service that wraps this and handles validating that products are found before creating the order and associating the records.
EDIT: OrderSerializer
Once you've verified that the join table records are being created properly. Check that your serializers are working, they have great documentation. I believe you can swap out your current products method in the OrderSerializer with this:
class OrderSerializer < ActiveModel::Serializer
attributes :id, :discount, :brute, :net, :payed, :payed_at, :products
def products
object.products.map do |product|
ProductSerializer.new(product).serializable_hash
end
end
end

Related

Rails API Controller Update Action -- How to add many-to-many

I have a Rails Api that feeds a Vue front end. I have two main models, Contacts and Outlets, with a many to many relationship via a join table ContactOutlets. I am trying to figure out how in the Contacts controller to add an association to an outlet.
I recognize, I could call the ContactOutlet create action separately, but it seems wasteful if Rails can handle this on the back end. I want vue to call contact#update once.
Contact Model:
class Contact < ApplicationRecord
has_many :contact_outlets
has_many :outlets, through: :contact_outlets
has_many :calls
validates_uniqueness_of :email
validates_uniqueness_of :name
end
Outlet Model:
class Outlet < ApplicationRecord
has_many :contact_outlets
has_many :contacts, through: :contact_outlets
has_many :calls
validates_uniqueness_of :website
end
ContactOutlet:
class ContactOutlet < ApplicationRecord
belongs_to :contact
belongs_to :outlet
validates_uniqueness_of :contact_id, :scope => :outlet_id
end
Contacts Controller:
class ContactsController < ApplicationController
before_action :set_contact, only: %i[ show update destroy ]
# GET /contacts
def index
#contacts = Contact.all
render json: #contacts, include: :outlets
end
# GET /contacts/1
def show
render json: #contact, include: :outlets
end
# POST /contacts
def create
#contact = Contact.new(contact_params)
if #contact.save
render json: #contact, status: :created, location: #contact
else
render json: #contact.errors, status: :unprocessable_entity
end
end
# PATCH/PUT /contacts/1
def update
if #contact.update(contact_params)
render json: #contact, include: :outlets
else
render json: #contact.errors, status: :unprocessable_entity
end
end
# DELETE /contacts/1
def destroy
#contact.destroy
end
private
# Use callbacks to share common setup or constraints between actions.
def set_contact
#contact = Contact.find(params[:id])
end
# Only allow a list of trusted parameters through.
def contact_params
params.require(:contact).permit(:name, :email, :bio, :image_url)
end
end
Solved this. In case anyone else is looking the models above are fine. made some adjustments to the contact_params to allow access to the outlets array. Then fixed the update action. Full controller code below:
class ContactsController < ApplicationController
before_action :set_contact, only: %i[ show update destroy ]
# GET /contacts
def index
#contacts = Contact.all
render json: #contacts, include: :outlets
end
# GET /contacts/1
def show
render json: #contact, include: :outlets
end
# POST /contacts
def create
#contact = Contact.new(contact_params)
if #contact.save
render json: #contact, status: :created, location: #contact
else
render json: #contact.errors, status: :unprocessable_entity
end
end
# PATCH/PUT /contacts/1
def update
if #contact.outlets
#contact.outlets.delete_all
end
if params[:outlets]
contactOutlets = params[:outlets]
contactOutlets.each do |outlet|
#contact.outlets << Outlet.find(outlet[:key])
end
end
if #contact.update(contact_params)
render json: #contact, include: :outlets
else
render json: #contact.errors, status: :unprocessable_entity
end
end
# DELETE /contacts/1
def destroy
#contact.destroy
end
private
# Use callbacks to share common setup or constraints between actions.
def set_contact
#contact = Contact.find(params[:id])
end
# Only allow a list of trusted parameters through.
def contact_params
params.require(:contact).permit(:name, :email, :bio, :image_url, outlet_ids:[])
end
end

rails : association tabel (has_and_belongs_to_many) not save any record

i use rails 5 , simple form. in my app there is a Category model and there is a OnlineProduct model. i dont know why when i want to add some categories to my OnlineProduct association table remain empty and don't change.
Category model:
class Category < ApplicationRecord
has_ancestry
has_and_belongs_to_many :internet_products
end
InternetProduct model:
class InternetProduct < ApplicationRecord
belongs_to :user
belongs_to :business
has_and_belongs_to_many :categories
end
InternetProduct controller:
def new
#internet_product = InternetProduct.new
end
def create
#internet_product = InternetProduct.new(internet_product_params)
respond_to do |format|
if #internet_product.save
format.html { redirect_to #internet_product, notice: 'Internet product was successfully created.' }
format.json { render :show, status: :created, location: #internet_product }
else
format.html { render :new }
format.json { render json: #internet_product.errors, status: :unprocessable_entity }
end
end
end
private:
def internet_product_params
params.require(:internet_product).permit(:name, :description, :mainpic, :terms_of_use,
:real_price, :price_discount, :percent_discount,
:start_date, :expire_date, :couponŲ€limitation, :slung,
:title, :meta_data, :meta_keyword, :enability, :status,
:like, :free_delivery, :garanty, :waranty, :money_back,
:user_id, :business_id,
categoriesŲ€attributes: [:id, :title])
end
and in the view only the part of who relate to categories :
<%= f.association :categories %>
all the categories list in view (form) but when i select some of them not save in database. in rails console i do this
p = InternetProduct.find(5)
p.categories = Category.find(1,2,3)
this save to database without any problem, what should i do ?
tanks for reading this
I found solution to solve this. when we use has_and_belong_to_many or any other relation , if you want to use collection select in simple_form , in the model also should be add this command for nesting form
accepts_nested_attributes_for :categories
also in the controller in related method for example in the new we should
def new
#internet_product = InternetProduct.new
#internet_product.categories.build
end

Ruby on rails - undefined method in show.html.erb

I am trying to display the name of the faculty instead of just the ID.
Everything else works except that part where I try to display the name of the faculty. I get an error saying "undefined method" .
I know I am doing something wrong, but I cannot figure it out at all even though I have been looking at this for hours. I am a completely beginner and I would really appreciate your help.
Thank you.
show.html.erb
<p id="notice"><%= notice %></p>
<!-- notice is a ruby method, and its results comes here inside the tags
used when you want the errow page to show on the next page -->
<p>
<strong>Name:</strong>
<%= #student.name %>
</p>
<p>
<strong>Faculty:</strong>
<%= #student.faculty_id %>
<%= #name.faculty_id %>
</p>
<strong>Grade:</strong>
<%= #student.grade%>
</p>
<%= link_to 'Edit', edit_student_path(#student) %> |
<%= link_to 'Back', students_path %>
student.rb
class Student < ActiveRecord::Base
belongs_to :faculty
end
class Name < ActiveRecord::Base
belongs_to :faculty
end
faculty.rb
class Faculty < ActiveRecord::Base
has_many :student
# belongs_to :faculty
has_many :name
end
This is my students_controler.rb
class StudentsController < ApplicationController
before_action :set_student, only: [:show, :edit, :update, :destroy]
# GET /students
# GET /students.json
def index
#students = Student.all
end
# GET /students/1
# GET /students/1.json
def show
end
# GET /students/new
def new
#student = Student.new
end
# GET /students/1/edit
def edit
end
# POST /students
# POST /students.json
def create
#student = Student.new(student_params)
respond_to do |format|
if #student.save
format.html { redirect_to #student, notice: 'Student was successfully created.' }
format.json { render :show, status: :created, location: #student }
else
format.html { render :new }
format.json { render json: #student.errors, status: :unprocessable_entity }
end
end
end
# PATCH/PUT /students/1
# PATCH/PUT /students/1.json
def update
respond_to do |format|
if #student.update(student_params)
format.html { redirect_to #student, notice: 'Student was successfully updated.' }
format.json { render :show, status: :ok, location: #student }
else
format.html { render :edit }
format.json { render json: #student.errors, status: :unprocessable_entity }
end
end
end
# DELETE /students/1
# DELETE /students/1.json
def destroy
#student.destroy
respond_to do |format|
format.html { redirect_to students_url, notice: 'Student was successfully destroyed.' }
format.json { head :no_content }
end
end
private
# Use callbacks to share common setup or constraints between actions.
def set_student
#student = Student.find(params[:id])
end
# Never trust parameters from the scary internet, only allow the white list through.
def student_params
params.require(:student).permit(:name, :faculty_id)
end
end
The error is undefined method `faculty_id' for nil:NilClass
I am trying to display the name of the faculty instead of just the ID
Since you're a beginner, let me explain it for you...
--
You're currently calling #student.faculty_id
This is the foreign_key of the #student object -- the identifier which links this student object to the appropriate faculty object.
In short, it means that this attribute is a part of the student schema -- you want one which is part of the faculty schema. Thus, you either need to use delegate to call the name attribute from faculty, or just call it directly:
#student.faculty.name
There are deeper problems with your model associations.
The above is how they should be set up:
#app/models/student.rb
class Student < ActiveRecord::Base
belongs_to :faculty
end
#app/models/faculty.rb
class Faculty < ActiveRecord::Base
has_many :students
end
The above will allow you to call the following:
#app/controllers/students_controller.rb
class StudentsController < ApplicationController
def show
#student = Student.find params[:id]
end
end
#app/views/students/view.html.erb
<%= #student.faculty.name %>
You must remember that Rails works on top of a relational database. This works by allowing you to call related objects by virtue of their foreign key.
I can explain more if required.
<%= #name.faculty_id %> won't work.
In your controller eager load the faculty
def show
#student = Student.includes(:faculty)
end
Either do
<% if #student.faculty.present? %>
<%= #student.faculty.name %>
<% end %>
or you could get the faculty in the controller and assign it to a variable
def show
#student = Student.includes(:faculty)
#faculty = #student.faculty
end
Then you can use that
<% if #faculty.present? %>
<%= #faculty.name %>
<% end %>
Error which I see is
class Faculty < ActiveRecord::Base
has_many :student
# belongs_to :faculty
has_many :name
end
Should be
class Faculty < ActiveRecord::Base
has_many :students
# belongs_to :faculty
has_many :names
end
I don't know your error due to this is or not but has_many is not with singular form

Creating a join table record through two different controllers in rails

I'm trying to get my head around the best way to add a record to a join table through alternative controllers in rails.
I have various models in my app that will require this, but I'm focusing on these two first before I transcribe the method into others, so shall use this as the example. I have a Venue and Interest model which are to be connected through VenuesInterests model (it has a couple of extra optional attributes so isn't a HABTM relationship). A user can admin a Venue instance and/or an Interest instance and therefore there should be an ability to select Venues to attach to an Interest and likewise Interests to attach to a Venue. This should be done with an Add Venues link on the Interest instance view and an Add Interests link on the Venue instance view. This would then take the user to a list of the relevant instances for them to select ones they would like to select.
Here are my models:
Venue.rb
class Venue < ActiveRecord::Base
has_many :interests, through: :venue_interests
has_many :venues_interests, dependent: :destroy
accepts_nested_attributes_for :venues_interests, :allow_destroy => true
end
Interest.rb
class Interest < ActiveRecord::Base
has_many :venues, through: :venue_interests
has_many :venues_interests, dependent: :destroy
end
VenuesInterests.rb
class VenuesInterest < ActiveRecord::Base
belongs_to :interest
belongs_to :venue
validates :interest_id, presence: true
validates :venue_id, presence: true
end
This all seems fine, however it's the controller and views that I'm struggling with. I've tried adding an extra method add_interest to the Venues controller to do the job of the create method in the VenuesInterests controller, so that there will be a different view when adding Venues to an Interest than there would be adding Interests to a Venue, otherwise I don't know how I would do this. My current Venues controller is as follows:
VenuesController.rb:
class VenuesController < ApplicationController
before_filter :authenticate_knocker!, only: [:new, :edit, :create, :update, :destroy]
respond_to :html, :json
def index
#venues = Venue.all.paginate(page: params[:page]).order('created_at DESC')
end
def show
#venue = Venue.find(params[:id])
#hash = Gmaps4rails.build_markers(#venue) do |venue, marker|
marker.lat venue.latitude
marker.lng venue.longitude
marker.infowindow venue.name
end
end
def new
#venue = Venue.new
end
def edit
#venue = Venue.find(params[:id])
end
def create
#venue = current_knocker.venues.create(venue_params)
respond_to do |format|
if #venue.save!
format.html { redirect_to #venue, notice: 'Venue was successfully created.' }
format.json { render json: #venue, status: :created, location: #venue }
else
format.html { render action: "new" }
format.json { render json: #venue.errors, status: :unprocessable_entity }
end
end
end
def update
#venue = Venue.find(params[:id])
#venue.update_attributes(venue_params)
respond_to do |format|
if #venue.update_attributes(venue_params)
format.html { redirect_to(#venue, :notice => 'Your Venue was successfully updated.') }
format.json { respond_with_bip(#venue) }
else
format.html { render :action => "edit" }
format.json { respond_with_bip(#venue) }
end
end
end
def destroy
end
def add_interests
#venues_interests = VenuesInterest.new
#interests = Interests.all.paginate(page: params[:page]).order(:name)
end
private
def venue_params
params.require(:venue).permit(:admin... etc)
end
end
This isn't currently working as I'm not sure how to reference other classes within a controller, but the important thing I'd like to know is is there a better way to do this or am I (kind of) on the right track? If anyone has a good method (perhaps a jQuery plugin) for allowing multiple selection of instances for the view, that would be great too!
In my opinion, I would take advantage of the existing update method to add the relationship between Interest and Venue. I can do like this:
def update
#venue = Venue.find(params[:id])
#venue.update_attributes(params[:venue_params])
if params[:interest_ids].present?
#venue.interests = Interest.where(id: params[:interest_ids])
#venue.save
end
#more code to handle the rendering
end

Rails 3.2 Associations and :include

I have some troubles to make an association between 2 tables.
I have Users who can write Posts
Here is my migration file :
class LinkUsersAndPosts < ActiveRecord::Migration
def change
add_column :posts, :user_id, :integer
add_index :posts, :user_id
end
end
My models :
class Post < ActiveRecord::Base
attr_accessible :content, :title
belongs_to :user
end
class User < ActiveRecord::Base
attr_accessible :email, :login, :password, :password_confirmation, :rights
has_many :posts
end
My controller :
class PostsController < ApplicationController
respond_to :json
def index
#posts = Post.includes(:user).all
respond_with #posts
end
def create
#post = Post.new(params[:post])
#post.user = current_user
if #post.save
render json: #post, status: :created, location: #post
else
render json: #post.errors, status: :unprocessable_entity
end
end
end
The posts is correctly created, the user_id is set all is fine.
The problem is when i want to retrieve the list of posts including user, the list of posts is retrieve, but i havn't any data on the user except his id.
Response :
[
{
"content":"jksdjd",
"created_at":"2013-08-31T09:03:01Z",
"id":11,"title":"kdjs",
"updated_at":"2013-08-31T09:03:01Z",
"user_id":4
},
{
"content":"tez2",
"created_at":"2013-08-31T09:16:45Z",
"id":12,
"title":"test2",
"updated_at":"2013-08-31T09:16:45Z",
"user_id":4
}
]
By default a JSON response won't return any associated models. You need to specify the other models you want returned. So in your case, you can do this:
render json: #post => #post.to_json(:include => :user), status: :created, location: #post
Also see:
http://api.rubyonrails.org/classes/ActiveModel/Serializers/JSON.html
Rails Object Relationships and JSON Rendering

Resources