Access another models attributes through has_many - ruby-on-rails

I'm looking to be able to call the values from another table this way in the view.
<%= #team.r1.players.full_name %>
<%= #team.r2.players.full_name %>
...
<%= #team.r2.players.full_name %>
r1 to r10 are columns that hold player IDs.
The models are pretty simple and look like this.
class Player < ApplicationRecord
has_and_belongs_to_many :teams
end
class Team < ApplicationRecord
belongs_to :user
has_many :players
end
The error that results is undefined method `players' for 3:Integer. I've attempted trying singular and plural versions to pull it but same error occurs. Looks like you might be able to do a join table but that wouldn't be ideal or doing a has_many :through association.
Schema
create_table "players", force: :cascade do |t|
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.string "full_name"
end
create_table "team_players", force: :cascade do |t|
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
create_table "teams", force: :cascade do |t|
t.bigint "user_id"
t.string "team_name"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.integer "r1"
t.integer "r2"
t.integer "r3"
t.integer "r4"
t.integer "r5"
t.integer "r6"
t.integer "r7"
t.integer "r8"
t.integer "r9"
t.integer "r10"
t.index ["user_id"], name: "index_teams_on_user_id"
end
And I set up updating/adding players on a team this way.
<%= f.collection_select(:r1, Player.all, :id, :full_name , {required: true, include_blank: false}, {class: 'uk-input'}) %>

As ConorB said in comments, the r1, r2, etc. bit seems weird. If you want to limit the number of players on a team, I would suggest you do that in code, not in your data structures.
As a note: In your Team model you say:
class Team < ApplicationRecord
belongs_to :user
has_many :players
end
According to the docs:
A has_many association indicates a one-to-many connection with another model.
This is not your case. You have a many-to-many cardinality between Team and Player. So, using has_many without through is a mistake in your case.
What you should have is:
class Player < ApplicationRecord
has_many :team_players
has_many :teams, through: :team_players
end
class Team < ApplicationRecord
has_many :team_players
has_many :players, through: :team_players
end
class TeamPlayer < ApplicationRecord
belongs_to :team
belongs_to :player
end
And then do:
<% #team.players.each do |player| %>
<%= player.full_name %>
<% end %>
You could decide to use has_and_belongs_to_many, in which case you would need a teams_players table, but no TeamPlayer model. (Personally, I never use HABTM, but that's a purely personal preference.) See the guide for more information.

Related

Creating Custom Friendship Associations Based Around an "Event" Model

I've been researching friendship models using roles, custom associations, etc. But I haven't been able to connect my project to the concepts in a clear way.
I want a "User" to be able to create an event I'm calling a "Gather". A User can also attend a Gather created by other Users. By attending a Gather, the "User" can also be a "Gatherer".
The list of Gatherers will technically be considered friends of the "creator". This is how far I've gotten:
Models:
User
Gather
Gatherer (?)
User
class User < ApplicationRecord
has_many :gathers_as_creator,
foreign_key: :creator_id,
class_name: :Gather
has_many :gathers_as_gatherer,
foreign_key: :gatherer_id,
class_name: :Gather
end
Gather
class Gather < ApplicationRecord
belongs_to :creator, class_name: :User
belongs_to :gatherer, class_name: :User
end
My question is, do I need to a join table, such as Gatherer, to allow multiple attendees and then later pull a friend list for the user/creator ?
Gatherer
belongs_to :gather_attendee, class_name: "User"
belongs_to :attended_gather, class_name: "Gather"
Here's what I think that schema would look like:
create_table "gatherers", force: :cascade do |t|
t.bigint "attended_gather_id"
t.bigint "gather_attendee_id"
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
t.index ["attended_gather_id"], name: "index_gatherers_on_attended_gather_id"
t.index ["gather_attendee_id"], name: "index_gatherers_on_gather_attendee_id"
end
Help, my head is spinning trying to understand the connections and how to proceed.
Previous planning:
Schema:
create_table "activities", force: :cascade do |t|
t.string "a_type"
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
end
create_table "gatherers", force: :cascade do |t|
t.bigint "attended_gather_id"
t.bigint "gather_attendee_id"
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
t.index ["attended_gather_id"], name: "index_gatherers_on_attended_gather_id"
t.index ["gather_attendee_id"], name: "index_gatherers_on_gather_attendee_id"
end
create_table "gathers", force: :cascade do |t|
t.integer "creator_id"
t.integer "activity_id"
t.text "gather_point"
t.boolean "active"
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
end
create_table "interest_gathers", force: :cascade do |t|
t.string "gather_id"
t.string "interest_id"
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
end
create_table "interests", force: :cascade do |t|
t.string "i_type"
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
end
create_table "users", force: :cascade do |t|
t.string "username"
t.string "img"
t.string "first_name"
t.string "last_name"
t.string "state"
t.string "city"
t.string "bio"
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
end
add_foreign_key "gatherers", "gathers", column: "attended_gather_id"
add_foreign_key "gatherers", "users", column: "gather_attendee_id"
end
class User < ActiveRecord::Base
has_many :gatherers, foreign_key: gather_attendee_id
has_many :attended_gathers, through: :gatherers
has_many :created_gathers, foreign_key: :creator_id, class_name: "Gather"
end
class Gather < ActiveRecord::Base
has_many :gatherers, foreign_key: :attended_gather_id
has_many :attendees, through: :gatherers, source: :gather_attendee
belongs_to :creator, class_name: "User"
end
class Gatherer < ActiveRecord::Base
belongs_to :gather_attendee, class_name: "User"
belongs_to :attended_gather, class_name: "Gather"
end
The naming here is not great. When naming your models choose nouns as models represent the actual things in your buisness logic - choosing verbs/adverbs makes the names of your assocations very confusing.
class User < ApplicationRecord
has_many :gatherings_as_creator,
class_name: 'Gathering',
foreign_key: :creator_id
has_many :attendences
has_many :gatherings,
through: :attendences
end
# think of this kind of like a ticket to an event
# rails g model Attendence user:references gathering:references
class Attendence < ApplicationRecord
belongs_to :user
belongs_to :gathering
end
# this is the proper noun form of gather
class Gathering < ApplicationRecord
belongs_to :creator,
class_name: 'User'
has_many :attendences
has_many :attendees,
though: :attendences,
class_name: 'User'
end
My question is, do I need to a join table, such as Gatherer, to allow multiple attendees and then later pull a friend list for the user/creator ?
Yes. You always need a join table to create many to many assocations. Gatherer is a pretty confusing name for it though as that's a person who gathers things.
If you want to get users attending Gatherings created by a given user you can do it through:
User.joins(attendences: :groups)
.where(groups: { creator_id: user.id })
You're on the right track.
If I understand what you're looking for correctly, you want a Gather to have many Users and a User to have many Gathers (for the attending piece). So you need a join table like this (this is similar to your gatherers table, but is in a more conventional Rails style):
create_join_table :gathers, :users do |t|
t.index [:gather_id, :user_id]
t.index [:user_id, :gather_id]
end
And then you'd want your User model to be like this:
class User < ApplicationRecord
has_many :gathers_as_creator, foreign_key: :creator_id, class_name: "Gather"
has_and_belongs_to_many :gathers
end
class Gather < ApplicationRecord
belongs_to :creator, class_name: "User"
has_and_belongs_to_many :users
end
(You can change the name of that :users association if you really want, by specifying extra options -- I just like to keep to the Rails defaults as much as I can.)
That should be the bulk of what you need. If you want to pull all the friends of a creator for a specific gather, you would just do gather.users. If you want to pull all of the friends of a creator across all their gathers, that will be:
creator = User.find(1)
friends = User.joins(:gathers).where(gathers: { creator: creator }).all

Rails Associations Issue

I have an issue seeding my data for some reason.
I feel like something is off with my model associations.
Anyone sees what's wrong?
I have 4 models - as you can see,
and I also attached the schema and the error.
Thanks for helping!
MODELS
class User < ApplicationRecord
has_secure_password
has_many :cuisines
has_many :dishes, through: :cuisine
has_many :comments
end
class Dish < ApplicationRecord
belongs_to :cuisine
has_many :comments
belongs_to :user, through: :cuisine
end
class Cuisine < ApplicationRecord
belongs_to :user
has_many :dishes
end
class Comment < ApplicationRecord
belongs_to :user
belongs_to :dish
end
**SCHEMA**
create_table "comments", force: :cascade do |t|
t.string "content"
t.integer "user_id"
t.integer "dish_id"
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
end
create_table "cuisines", force: :cascade do |t|
t.string "country"
t.string "picture"
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
t.integer "user_id"
end
create_table "dishes", force: :cascade do |t|
t.string "name"
t.string "picture"
t.string "ingredients"
t.string "directions"
t.string "cook_time"
t.string "yield"
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
t.integer "cuisine_id"
end
create_table "users", force: :cascade do |t|
t.string "name"
t.string "username"
t.string "password_digest"
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
end
end
**SEED FILE**
User.create(name: "Adi", username: "adi3", password: "password")
italian = Cuisine.create(country: "Italy", picture: "https://www.delonghi.com/Global/recipes/multifry/pizza_fresca.jpg", user_id:1)
pizza = italian.dishes.create(name: "Pizza", picture: "https://d1uz88p17r663j.cloudfront.net/original/4274048cd5f17c49dfee280f77a3739d_Cheese-Pizza_HB-2.jpg", ingredients: "flour, water, yeast, oil, salt, cheese, olives", directions: "mix everything together and add toppings", cook_time: "30 min", yield: "1 big pizza", cuisine_id:1)
adi = User.first
adi_pasta = adi.dishes.create(name: "Pomodoro Pasta", picture: "https://lh3.googleusercontent.com/_mkIjqOA29bzih8kz98RjBqf6KbLaan2ReyAzM2-Vj7SPDWGF24vPLz3zdTiwRdHaDdn6ed5kHdUUWkyoOQ83ZE=s640-c-rw-v1-e365", ingredients: "olive oil, garlic, basil, tomato, pasta", directions: "mix everything, cook pasta and top it up", cook_time: "20 min", yield: "4 portions", cuisine_id:1)
adi.comments.create(content: "Great easy tomato pasta", dish: adi_pasta, user_id:1, dish_id:2)
**ERROR**
// ☆♥☆ > rails db:seed
rails aborted!
ArgumentError: Unknown key: :through. Valid keys are: :class_name, :anonymous_class, :primary_key, :foreign_key, :dependent, :validate, :inverse_of, :strict_loading, :autosave, :required, :touch, :polymorphic, :counter_cache, :optional, :default
/Users/username/project-backend/app/models/dish.rb:4:in `class:Dish'
The issue has nothing to do with your seed file. In your dish class you have defined a belongs_to with the through option:
class Dish < ApplicationRecord
belongs_to :cuisine
has_many :comments
belongs_to :user, through: :cuisine
end
This is not possible since belongs_to places the foreign key on this models table. What you intended to use is a has_one association:
class Dish < ApplicationRecord
belongs_to :cuisine
has_many :comments
has_one :user, through: :cuisine
end
But honestly it does not make very much sense to use indirect assocation here in the first place - why would the dishes created be associated automatically with the user that created the cuisine? Instead you just want a standard belongs_to association:
class Dish < ApplicationRecord
belongs_to :cuisine
belongs_to :user
has_many :comments
end
I would call the association something more descriptive though like creator.

Get value from join model

I'm starting out with Rails (version 5.1.7) and am having some trouble getting my head around the has_many :through associations, specifically how to retrieve values from the intermediary join model.
So I have the following models:
#app/models/project.rb
class Project < ApplicationRecord
belongs_to :owner, class_name: 'User', foreign_key: 'user_id'
has_many :project_users
has_many :users, through: :project_users
end
#app/models/project_user.rb
class ProjectUser < ApplicationRecord
belongs_to :user
belongs_to :project
end
#app/models/user.rb
class User < ApplicationRecord
has_many :project_users
has_many :projects, through: :project_users
has_many :projects, inverse_of: 'owner'
end
The requirements are:
multiple projects and multiple users, with a many-to-many relationship between them;
the relationship needs specific attributes (role, hourly fee, etc), which is why I didn't opt for a HABTM;
each project needs to have a single owner;
both users and owner are items derived from the User model (and a single user may at the same time be a project 'user' and 'owner').
Now here's my database:
#db/schema.rb
ActiveRecord::Schema.define(version: 20210409064744) do
create_table "project_users", force: :cascade do |t|
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.integer "user_id"
t.integer "project_id"
t.string "role"
t.decimal "fee", precision: 10, scale: 2
t.index ["project_id"], name: "index_project_users_on_project_id"
t.index ["user_id"], name: "index_project_users_on_user_id"
end
create_table "projects", force: :cascade do |t|
t.string "name"
t.text "description"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.integer "user_id"
t.index ["user_id"], name: "index_projects_on_user_id"
end
create_table "users", force: :cascade do |t|
t.string "login"
t.string "firstname"
t.string "lastname"
t.string "password_digest"
t.string "email"
t.string "avatar"
t.string "role"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
end
I have no trouble getting values like <%= #project.name %> or
<strong>Project members:</strong>
<ul>
<% #project.users.each do |proj_user| %>
<li><%= link_to proj_user.name, user_path(proj_user) %></li>
<% end %>
</ul>
in my app/views/projects/show.html.erb.
But how in the world can I get the user's fee for said project? Specifically, how can I retrieve the value of fee from the project_users table?
For clarity, here's the project_users table.
id
created_at
updated_at
user_id
project_id
role
fee
1
2021-04-09 06:54:21.231836
2021-04-09 06:54:21.231836
2
1
member
300
2
2021-04-09 06:54:21.233715
2021-04-09 06:54:21.233715
3
1
member
300
3
2021-04-09 06:54:21.251290
2021-04-09 06:54:21.251290
2
2
member
300
4
2021-04-09 06:54:21.254056
2021-04-09 06:54:21.254056
3
2
member
250
5
2021-04-09 06:54:21.273320
2021-04-09 06:54:21.273320
5
3
member
300
Thanks in advance!
probably better here to change your iteration to iterate through project_users and not users then call users from project_users since it has all the info
<% #project.project_users.each do |proj_user| %>
<li><%= link_to proj_user.user.name, user_path(proj_user.user) %></li>
<% end %>
then you will be able to call proj_user.fee directly
you can also delegate user_name to project_user model like so if you want
class ProjectUser < ApplicationRecord
belongs_to :user
belongs_to :project
delegate :name, to: :user, prefix: true
end
so it would become:
<% #project. project_users.each do |proj_user| %>
<li><%= link_to proj_user.user_name, user_path(proj_user.user) %></li>
<% end %>
you can read more about delegate here https://apidock.com/rails/Module/delegate

Unitialized constant User:Bookings when trying to add data into a join table

I have a User table and a Booking Table that is linked by a create_join_table what holds the user id and booking ids. When a user books a room, i need the id of both the user and new booking to go into that. I am getting the error above and im not sure why.
I have looked online and saw something similar, their class names were plural however I don't think I have that.
booking.rb
class Booking < ApplicationRecord
enum room_type: ["Basic Room", "Deluxe Room", "Super-Deluxe Room", "Piton Suite"]
has_many :join_tables
has_many :users, through: :join_tables
end
user.rb
class User < ApplicationRecord
has_secure_password
validates :email, format: {with: URI::MailTo::EMAIL_REGEXP}, presence: true, uniqueness: true
has_many :join_tables
has_many :bookings, through: :join_tables
end
join_table.rb
class JoinTable < ApplicationRecord
belongs_to :users
belongs_to :bookings
end
bookings_controller.rb
def create
#booking = Booking.create(booking_params)
current_user.bookings << #booking ##Where the error happens
db/schema
ActiveRecord::Schema.define(version: 2019_12_13_181019) do
create_table "bookings", force: :cascade do |t|
t.integer "room_type"
t.date "check_in"
t.date "check_out"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
create_table "join_tables", force: :cascade do |t|
t.integer "users_id"
t.integer "bookings_id"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["bookings_id"], name: "index_join_tables_on_bookings_id"
t.index ["users_id"], name: "index_join_tables_on_users_id"
end
create_table "users", force: :cascade do |t|
t.string "name"
t.string "email"
t.string "password_digest"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
end
I have just tried to reproduce your problem and I have a similar exception
irb(main):003:0> User.first.bookings
NameError (uninitialized constant User::Bookings)
but, when I change
belongs_to :users
belongs_to :bookings
to
belongs_to :user
belongs_to :booking
in app/models/join_table.rb everything works as expected.
This is how I created the JoinTable model
$ rails generate model JoinTable
class CreateJoinTables < ActiveRecord::Migration[6.0]
def change
create_table :join_tables do |t|
t.references :user
t.references :booking
t.timestamps
end
end
end
As you can see in the belongs_to docs, it is used in the singular form most of the time.

Has_many through query

I have 2 models (Books and Authors) and a third table joining them (has_many through association).
I am trying to implement search in my app and run a query on both tables. My query looks like this and I cannot figure out the problem:
Book.includes(:authors, :author_books).where("books.title LIKE ? OR authors.name = LIKE ?", "%#{book}%", "%#{book}%")
This is the error that I get running it:
PG::UndefinedTable: ERROR: missing FROM-clause entry for table "authors"
SELECT "books".* FROM "books" WHERE (books.title LIKE '%Harry%' OR authors.name = LIKE '%Harry%')
Here is my schema of the three tables:
create_table "author_books", force: :cascade do |t|
t.bigint "author_id"
t.bigint "book_id"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["author_id"], name: "index_author_books_on_author_id"
t.index ["book_id"], name: "index_author_books_on_book_id"
end
create_table "authors", force: :cascade do |t|
t.string "name"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
create_table "books", force: :cascade do |t|
t.string "title"
t.text "description"
t.string "image"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.integer "rating"
t.string "critics"
t.float "price"
end
author_book.rb
class AuthorBook < ApplicationRecord
validates_presence_of :author, :book
belongs_to :author
belongs_to :book
end
author.rb
class Author < ApplicationRecord
validates :name, uniqueness: true
has_many :author_book
has_many :books, through: :author_book
end
book.rb
class Book < ApplicationRecord
validates :title, uniqueness: true, :case_sensitive => false
has_many :author_book
has_many :authors, through: :author_book
has_many :categories, through: :category_book
def self.search_book(book)
if book
Book.joins(:authors, :author_books).includes(:authors, :author_books).where("books.title LIKE ? OR authors.name = LIKE ?", "%#{book}%", "%#{book}%")
end
end
end
I call this search_book method in my book controller like so:
def search
#books = Book.search_book(params[:book])
end
Some help, please?
Thanks!
From the docs
If you want to add conditions to your included models you’ll have to
explicitly reference them.
That said, you need to add references(:authors) to your query like below to resolve the error
Book.includes(:authors, :author_books).where("books.title LIKE ? OR authors.name = LIKE ?", "%#{book}%", "%#{book}%").references(:authors)
Update:
Can't join 'Book' to association named 'author_books'; perhaps you
misspelled it?
You should replace has_many :author_book with has_many :author_books and through: :author_book with through: :author_books
You forgot to join authors and author_books to your relation. includes loads both :author and :author_books but in separate queries.
Try this:
Book.joins(:authors, :author_books).includes(:authors, :author_books).where("books.title LIKE ? OR authors.name = LIKE ?", "%#{book}%", "%#{book}%")

Resources