RoR API how to use the post for asociations - ruby-on-rails

I am string an API project using RoR 6. I have this models:
class Province < ApplicationRecord
validates :province, presence: true, length: {minimum:5}, uniqueness: true
has_many :cities, dependent: :destroy
end
class City < ApplicationRecord
validates :city, presence: true, length: {minimum: 5}, uniqueness: true
belongs_to :province
end
the migrations:
class CreateProvinces < ActiveRecord::Migration[6.1]
def change
create_table :provinces do |t|
t.string :province
t.timestamps
end
end
end
class CreateCities < ActiveRecord::Migration[6.1]
def change
create_table :cities do |t|
t.string :city
t.references :province, null: false, foreign_key: true
t.timestamps
end
end
end
the controller method:
def create
#city = City.new(city_params)
if #city.save
render json: #city, status: :created, location: #city
else
render json: #city.errors, status: :unprocessable_entity
end
end
private
def city_params
params.require(:city).permit(:city, :province_id)
end
and the routes:
Rails.application.routes.draw do
resources :provinces, defaults: { format: :json } do
resources :cities, defaults: { format: :json }
end
end
the thing is this, when running:
curl --location --request POST 'http://127.0.0.1:3000/provinces/1/cities' \
--header 'Content-Type: application/json' \
--data-raw '{
"city": "New City",
"province": 1
}'
{ "province": [ "must exist" ] }
The province exists. But there is no way to insert it. If I use the rails console and run:
p=Province.find(1)
c=City.new({city: 'New City', province: p})
c.save
it works as expected. How to solve this?

When dealing with a nested route you want to find the parent resource first and then create the nested resource off it:
class ProvincesController < ApplicationController
# POST /provinces/1/cities
def create
#province = Province.find(params[:province_id])
#city = #province.cities.new(city_params)
if #city.save
render json: #city, status: :created, location: #city
else
render json: #city.errors, status: :unprocessable_entity
end
end
private
def city_params
params.require(:city).permit(:city)
end
end
Also calling the columns provinces.province and cities.city makes a lot less sense then using provinces.name and cities.name.

Related

Preventing duplicates in a has_many through association?

I've got two tables/models (Users and Concerts) that has a join-table model of Posts. This is for a 'ticketmaster' style marketplace so a User can make a Post on any given Concert, and a Concert can also have info on a given User through the Post that this User made.
The problem is that I have user duplicates on my /concerts and concert duplicates on my users; I'm not sure why either. Below are the JSON output of /concerts and /users.
/concerts is here:
{
"id": 45,
"date": "2023-01-19T00:00:00.000Z",
"location": "Brooklyn Steel",
"image": "https://i.imgur.com/SmFrzTC.jpg",
"artist_id": 33,
"artist": {
"id": 33,
"name": "Adele",
"image": "https://i.imgur.com/zmGbfKS.jpg",
"genre": "Pop"
},
"posts": [],
"users": [
{
"id": 257,
"username": "onlineguy1",
"email": "eusebia_larson#wilderman.co"
},
{
"id": 257,
"username": "onlineguy1",
"email": "eusebia_larson#wilderman.co"
},
{
"id": 273,
"username": "L0V3MUSIC",
"email": "lulu_lemke#johns.name"
}
]
},
For /users, it looks like this and you can see the issue more:
{
"id": 257,
"username": "onlineguy1",
"email": "eusebia_larson#wilderman.co",
"posts": [],
"concerts": [
{
"id": 45,
"date": "2023-01-19T00:00:00.000Z",
"location": "Brooklyn Steel",
"image": "https://i.imgur.com/SmFrzTC.jpg",
"artist_id": 33
},
{
"id": 45,
"date": "2023-01-19T00:00:00.000Z",
"location": "Brooklyn Steel",
"image": "https://i.imgur.com/SmFrzTC.jpg",
"artist_id": 33
},
{
"id": 46,
"date": "2024-05-23T00:00:00.000Z",
"location": "Mao Livehouse",
"image": "https://i.imgur.com/CghhYym.jpg",
"artist_id": 33
},
{
"id": 46,
"date": "2024-05-23T00:00:00.000Z",
"location": "Mao Livehouse",
"image": "https://i.imgur.com/CghhYym.jpg",
"artist_id": 33
},
{
"id": 47,
"date": "2023-04-29T00:00:00.000Z",
"location": "Madison Square Garden",
"image": "https://i.imgur.com/0gd1dD0.jpg",
"artist_id": 33
},
{
"id": 47,
"date": "2023-04-29T00:00:00.000Z",
"location": "Madison Square Garden",
"image": "https://i.imgur.com/0gd1dD0.jpg",
"artist_id": 33
},
]
},
Below are my post model, my user model, my concert model FWIW.
class User < ApplicationRecord
has_secure_password
validates_uniqueness_of :username, presence: true
# validates :username, presence: true, uniqueness: true
validates :password, length: { minimum: 8, maximum: 254}
validates_presence_of :email
validates_format_of :email, with: URI::MailTo::EMAIL_REGEXP
# validates :my_email_attribute, email: true, presence: true
has_many :posts
has_many :concerts, through: :posts
end
class Post < ApplicationRecord
belongs_to :user
belongs_to :concert
validates :body, presence: true
validates :tickets, presence: true, numericality: { greater_than: 0 }
end
class Concert < ApplicationRecord
belongs_to :artist
has_many :posts
has_many :users, through: :posts
end
If anybody's got a step in the right direction, I'll gladly take it because I can't figure it out. Been poring through docs but I've psyched myself out somewhere
EDIT: to include my Controllers, Serializers, and route.
Also, controllers here below, starting with Post:
class PostsController < ApplicationController
rescue_from ActiveRecord::RecordNotFound, with: :render_not_found_response
rescue_from ActiveRecord::RecordInvalid, with: :render_unprocessable_entity_response
def index
posts = Post.all
render json: posts
end
def show
post = Post.find_by!(id: params[:id])
render json: post, status: 200
end
def create
post = Post.create!(new_post_params)
render json: post, status: 201
end
# ## made this one to not-render duplicates but still rendered duplicates
# def create
# ## links the proper user to the post
# correct_user = User.find_by!(id: params[:user_id])
# ## links the proper concert to the post
# correct_concert = Concert.find_by!(id: params[:concert_id])
# newPost = Post.create!(
# id: params[:id],
# body: params[:body],
# tickets: params[:tickets],
# for_sale: params[:for_sale],
# concert_id: correct_concert.id,
# user_id: correct_user.id
# )
# render json: newPost, status: 201
# end
def update
post = Post.find_by!(id: params[:id])
if session[:user_id] === post[:user_id]
post.update!(
body: params[:body],
tickets: params[:tickets]
)
render json: post, status: 200
end
end
def destroy
post = Post.find_by!(id: params[:id])
if session[:user_id] === post[:user_id]
post.destroy
head :no_content
end
end
private
def new_post_params
params.require(:concert_id, :user_id, :for_sale, :tickets, :body)
end
def render_unprocessable_entity_response(invalid)
render json: { errors: invalid.record.errors.full_messages }, status: :unprocessable_entity
end
def render_not_found_response(invalid)
render json: { error: invalid.message }, status: :not_found
end
end
And here's for Users:
class UsersController < ApplicationController
rescue_from ActiveRecord::RecordNotFound, with: :render_not_found_response
rescue_from ActiveRecord::RecordInvalid, with: :render_unprocessable_entity_response
def index
users = User.all
render json: users
end
## get '/me'
def show
user = User.find_by!(id: session[:user_id]) ## changed it to User.find_by! for it to work
render json: user, status: 200
end
def create
user = User.create!(signup_user_params)
session[:user_id] = user.id
render json: user, status: :created
end
# # the original show
# def show
# user = User.find_by(id: session[:user_id])
# if user
# render json: user, status: 200
# else
# render json: user.errors.full_messages, status: :unprocessable_entity
# end
# end
# # the original create
# def create
# user = User.create(signup_user_params)
# if user.valid?
# session[:user_id] = user.id
# render json: user, status: :created
# else
# render json: user.errors.full_messages, status: :unprocessable_entity
# end
# end
# # update a specific user
# def update
# if user.update(user_params)
# render json: user
# else
# render json: user.errors, status: :unprocessable_entity
# end
# end
# # delete a specific user
# def destroy
# user.destroy
# end
private
def signup_user_params
params.permit(:username, :password, :password_confirmation, :email)
end
def render_unprocessable_entity_response(invalid)
render json: { errors: invalid.record.errors.full_messages }, status: :unprocessable_entity
end
def render_not_found_response(invalid)
render json: { error: invalid.message }, status: :not_found
end
end
And here's Concert:
class ConcertsController < ApplicationController
def index
concerts = Concert.all
render json: concerts
end
def show
concert = Concert.find_by!(id: params[:id])
render json: concert, status: 200
end
## finish after the duplicates issue
def create
## find the proper artist, and link the proper artist
end
end
Here's the Serializers:
class ConcertSerializer < ActiveModel::Serializer
attributes :id, :date, :location, :image, :artist_id
belongs_to :artist, serializer: ArtistSerializer
has_many :posts, serializer: PostSerializer
has_many :users, through: :posts, serializer: UserSerializer
end
class PostSerializer < ActiveModel::Serializer
attributes :id, :body, :for_sale, :tickets, :concert_id, :user_id
belongs_to :user, serializer: UserSerializer
belongs_to :concert, serializer: ConcertSerializer
end
class UserSerializer < ActiveModel::Serializer
attributes :id, :username, :email
has_many :posts, serializer: PostSerializer
has_many :concerts, through: :posts, serializer: ConcertSerializer
end
Here's routes.rb:
Rails.application.routes.draw do
#& Defines the root path route ("/")
#& root "articles#index"
##~ FOR THE ARTIST-CONCERTS-VENUES DISPLAYS
#& getting all the artists-concerts-users
get '/artists', to: "artists#index"
get '/artists/:id', to: "artists#show"
get '/concerts', to: "concerts#index"
get "/users", to: "users#index"
##~ FOR THE POSTS GET/CREATION/EDITS/DELETION
get '/posts', to: "posts#index"
post '/new_post', to: "posts#create"
patch '/update_post/:id', to: "posts#update"
delete '/delete_post/:id', to: "posts#destroy"
##~ THE LOGIN/LOGOUT ROUTES
#& to create a new user outright
post "/new_user", to: "users#create"
#& to login our user
post "/login", to: "sessions#create"
#& to keep the user logged in
get "/me", to: "users#show"
#& to log the user out
delete "/logout", to: "sessions#destroy"
##~ SESSION & COOKIES INFO
#& shows session_id and sessions info
get "/show_session", to: "application#show_session"
#& displays cookies
get "/cookies", to: "application#show_cookies"
# Routing logic: fallback requests for React Router.
# Leave this here to help deploy your app later!
get "*path", to: "fallback#index", constraints: ->(req) { !req.xhr? && req.format.html? }
end
The fact that posts: [] is shown I am going to assume is for brevity becuase there can be no users for a Concert without posts having elements.
Your issue is that you join User and Concert through Post, and it is reasonable to assume that a User may post more than once about a Concert is it not?
Given your current relationships and the fact that you are using ActiveModel::Serializer you are going to have to Override the Association method to return only distinct User/Concerts.
For Example:
class ConcertSerializer < ActiveModel::Serializer
attributes :id, :date, :location, :image, :artist_id
belongs_to :artist, serializer: ArtistSerializer
has_many :posts, serializer: PostSerializer
has_many :users, through: :posts, serializer: UserSerializer do
object.users.distinct
end
end
Note: I am not sure how this does not end up in a circular dependency as it appears it should (I don't use this library for APIs)
I managed to solve this by using the distinct property when defining my models.
class Concert < ApplicationRecord
belongs_to :artist
has_many :posts
has_many :users, -> { distinct }, through: :posts
end
By using --> {distinct}, I only got distinct (i.e. no repeats) objects rendered back in the JSON. Whether or not this is the most optimal way, I can't speak on but it definitely solved my original problem so I'm answering this question myself. You can read more here if you're stuck in the same boat.

Updating a model association with an invalid id does not raise an exception

Why when I try to update a model passing an invalid ID that references another model, an exception is not throw? It just do nothing and returns true.
Given the following migration:
create_table :conversations, id: :uuid do |t|
t.references :contact, type: :uuid, null: false, foreign_key: true
t.references :group, type: :uuid, foreign_key: true
t.references :agent, type: :uuid, foreign_key: true
t.timestamps
end
Controller
class Api::V1::ConversationsController < ApplicationController
respond_to :json
before_action :set_conversation, only: [:update]
def update
#conversation.update!(conversation_params)
render json: #conversation, status: :ok
rescue ActionController::ParameterMissing => e
render json: { error: e.message }, status: :unprocessable_entity
rescue ActionController::UnpermittedParameters => e
render json: { error: e.message }, status: :unprocessable_entity
end
private
def conversation_params
params.require(:conversation).permit(:group_id, :agent_id, :status)
end
def set_conversation
#conversation = Conversation.find_by_id(params[:id])
return unless #conversation.nil?
render json: { error: 'The conversation you are trying to update was not found.' }, status: :not_found
end
end
PUT request with payload :
{
"group_id": "123456",
}
The group_id is not updated, but neither an exception is thrown.
I would like that an exception to be raised, so I could handle it and render a proper JSON response.
EDIT
Conversation model
class Conversation < ApplicationRecord
belongs_to :contact
belongs_to :group, optional: true
belongs_to :agent, optional: true
validates_associated :contact
validates_associated :group
validates_associated :agent
end

change parameter in one direction in update function when making api

i'm trying to make API using Ruby on Rails 5 , i'm new on it so i used scaffold to generate tables so this my main table for trip
class CreateTrips < ActiveRecord::Migration[5.2]
def change
create_table :trips do |t|
t.string :nameOfDriver
t.string :status
t.timestamps
end
end
end
and this is my update function in controller
def update
if #trip.update(trip_params)
render json: #trip
else
render json: #trip.errors, status: :unprocessable_entity
end
end
i'm trying to add constrains first, to status to be "completed " or "ongoing " ... second check if it "completed" i can't change it to "ongoing"
this link for the full project
https://github.com/gnik2036/RubyTask
class Trip < ApplicationRecord
TRIP_STATUSES = %w[completed ongoin].freeze
INVALID_STATUS_CHANGES = [%w[completed ongoin]].freeze
validate :status_validity, on: :update
validates :status, presence: true, inclusion: { in: TRIP_STATUSES }
private
def status_validity
return unless status_changed?
if INVALID_STATUS_CHANGES.include? status_change
errors.add(:status, "Cannot be changed from #{status_was} to #{status}.")
end
end
end

Create a post with many-to-many relationship in Ruby on Rails

I have a structure very much for between category and yell. What I do is qeuro a call on POST type API with the following parameters:
{
"user_id":"1",
"title":"primeito",
"desciption":"de_novo",
"categories":[{"name":"eletro"},{"name":"domestic"},{"name":"new_category"}],
"yell_type":"novo",
"price":"10,00",
"payment_type":"boleto"
}
My structure is as follows:
My model yell:
#yell.rb
class Yell < ActiveRecord::Base
belongs_to :user, inverse_of: :yells
has_and_belongs_to_many :categories
end
model category:
#category.rb
class Category < ActiveRecord::Base
has_and_belongs_to_many :yells
end
method crete in controller yell:
#yells_controller.rb
def create
#yell = Yell.new(yell_params)
params[:categories].each do |rel|
#category = Category.find_by_name(rel[:name])
if #category
#only creates the relationship
else
#yell.categories.build(name: rel[:name]) #creates the relationship and category
end
end
if #yell.save
render json: #yell, status: :created, location: api_yell_path(#yell)
else
render json: #yell.errors, status: :unprocessable_entity
end
end
...
private:
def yell_params
params.require(:yell).permit(:title, :desciption, :price, :payment_type, :user_id, :yell_type, :categories)
end
So I created the table
class CreateCategoriesYellsJoinTable < ActiveRecord::Migration
def self.up
create_table :categories_yells, :id => false do |t|
t.integer :category_id
t.integer :yell_id
end
add_index :categories_yells, [:category_id, :yell_id]
end
def self.down
drop_table :categories_yells
end
end
I can make him create the categories, but does not know how to create only the relationship. Agluem can help me is the comment #only creates the relationship?
I need to do this check because the category name is unique
Also if someone know something more elegant way to do this, I accept suggestions
I am not quite sure I understood the last paragraph, but I think you need an intermediate table to join the two models first.
You would need to create a table like this:
class CreateCategoriesAndYells < ActiveRecord::Migration
def change
create_table :categories_yells, id: false do |t|
t.belongs_to :category, index: true
t.belongs_to :yell, index: true
end
end
end
Then you would need to update your controller to say something like this:
#yell.categories.build(category_params)
You would need also to pass the category parameters to the controller.
In order to do so I had to create a model to join my table:
model category_yell.rb
class CategoriesYell < ActiveRecord::Base
belongs_to :category
belongs_to :yell
end
and create my method was as follows:
def create
##yell = Yell.new(yell_params.except(:categories))
#yell = Yell.new({title: params[:title], desciption: params[:desciption], price: params[:price], user_id: params[:user_id], yell_type: params[:yell_type]})
if #yell.save
Array(params[:categories]).each do |rel|
#category = Category.find_by_name(rel[:name])
if #category
#categories_yells = CategoriesYell.new(category_id: #category.id, yell_id: #yell.id)
if #categories_yells.save
#yell.categories.build(id: #category.id, name: rel[:name])#only creates the relationship
else
render json: {status: 1, message:"relationship categoy not create", data: #yell.errors}, status: :unprocessable_entity
end
else
#yell.categories.create(name: rel[:name]) #creates the relationship and category
end
end
render json: {status: 0, message:"sucess", data: #yell}, status: :created
else
render json: {status: -1, message:"error", data: #yell.errors}, status: :unprocessable_entity
end
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