Preventing duplicates in a has_many through association? - ruby-on-rails

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.

Related

Need help validating query parameters

I have multiple methods within my controller that takes in query parameters. How can I validate that I am being passed in valid parameters? For example, for the index method, how can I make sure that I am getting an array of authorIds.
def index
author_ids_array = params[:authorIds].to_s.split(',')
posts = Post
.get_posts_by_user_id(author_ids_array)
.order(sort_column => sort_direction)
if posts
render json: { posts: posts }, status: :ok
else
render json: {error: posts.errors}, status: :unprocessable_entity
end
end
Or in this update method. How can I validate that I am getting a valid postId
def update
post = current_user.posts.find_by(id: params[:id])
if post.update!(post_params)
post_hash = post.as_json
post_hash.merge!(authorIds: params[:authorIds])
render json: {post: post_hash}, status: :ok
else
render json: {error: post.errors}, status: :unprocessable_entity
end
end
Update:
Post Model:
class Post < ApplicationRecord
# Associations
has_many :user_posts
has_many :users, through: :user_posts, dependent: :destroy
# Validations
validates :text, presence: true, length: { minimum: 3 }
validates :popularity, inclusion: { in: 0.0..1.0 }
def tags
if super
super.split(",")
end
end
def tags=(value)
if value.kind_of? Array
super value.join(",")
else
super value
end
end
def self.get_posts_by_user_id(user_id)
Post.joins(:user_posts).where(user_posts: { user_id: user_id })
end
end
User Model:
class User < ApplicationRecord
has_secure_password
# Associations
has_many :user_posts
has_many :posts, through: :user_posts, dependent: :destroy
# Validations
validates :username, :password, presence: true
validates :password, length: { minimum: 6 }
validates :username, uniqueness: true
end
User_post Model:
class UserPost < ApplicationRecord
belongs_to :user
belongs_to :post
end
You can use specific render like below user this in any method like
def index
return render body: params.inspect
.
.
end
user below code
return render body: params.inspect
so when you use index it will give you params which is passing
OR you can user below code in your application.html.erb above <%= yield%>
<%= debug(params) if Rails.env.development? %>
After your clarifications, your question remains unclear to me and it is difficult to guess what you're doing. But I understood that you want to ensure that params[:authorIds] or anything else is an array.
You can see if a given variable is an array the following way:
a = ["1","2"]
if a.is_a?(Array)
puts "is an array"
end
With params: params[:authorIds].is_a?(Array)
You can use byebug (before Rails 7) or debugger (for Rails 7) to inspect what a param is. As an example:
(ruby#whatever: cluster worker 1: 42779 [MyApp]#42793) params[:ids].class
Array

RoR API how to use the post for asociations

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.

"password_digest can't be blank" and "password can't be blank" errors

I'm trying to create a user via Postman as shown in the screenshot, but getting errors:
This is a rails app created with an --api option.
user.rb
class User < ApplicationRecord
validates :email, uniqueness: true
validates_format_of :email, with: /#/
validates :password_digest, presence: true
has_secure_password
end
users_controller.rb
class Api::V1::UsersController < ApplicationController
# GET /users/1
def show
render json: User.find(params[:id])
end
# POST /users
def create
#user = User.new(user_params)
if #user.save
render json: #user, status: :created
else
render json: #user.errors, status: :unprocessable_entity
end
end
private
def user_params
params.require(:user).permit(:email, :password)
end
end
routes.rb
Rails.application.routes.draw do
namespace :api, defaults: { format: :json } do
namespace :v1 do
resources :users, only: [:show, :create]
end
end
end
Your user_params method expects the attributes nested in a user hash. Change the response to:
{
"user": {
"email": "...",
"password": "..."
}
}
Btw the validates :password_digest, presence: true line is not needed because has_secure_password has validations build in and ensures internally that a password is present.

Access to associations from polymorphic model

I have next models:
class Post < ActiveRecord::Base
has_many :comments, as: :commentable
end
class Article < ActiveRecord::Base
has_many :comments, as: :commentable
end
class Comment < ActiveRecord::Base
belongs_to :commentable, polymorphic: true
end
Now I can get posts and articles via #comment.commentable, but I need via #comment.post or #comment.article. I found some solutions, but it's not working with nested attributes:
accepts_nested_attributes_for :article
accepts_nested_attributes_for :post
My comments_controller:
class CommentsController < ApplicationController
def create
#comment = Comment.new(comment_params)
if #comment.save!
head :ok, location: #comment
else
render json: #comment.errors, status: :unprocessable_entity
end
end
private
def comment_params
params.require(:comment).permit(
:text, text: {},
post_attributes: [:id, text: {}],
article_attributes: [:id, text: {}]
)
end
end
Post params from form like:
const params = {
comment: {
text,
post_attributes: [{
id: post ? post.id : null,
text,
}]
}
}
If post or article not exists it must be created. Or add one more model like here https://gist.github.com/runemadsen/1242485 — but I don't want to do that :(

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