How to setup basic Rails models associations? - ruby-on-rails

hey guys im working on a application where a devise user sign ups and logs in, Once the user logs in they can 'create a team' or 'join a team'. I have my associations set up like this
user.rb
class User < ApplicationRecord
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :validatable, :confirmable
validates_presence_of :phone, :city, :state, :street, :zip, presence: true, on: :create
belongs_to :team
end
team.rb
class Team < ApplicationRecord
has_many :users
end
and my tables are set up
schema.rb
create_table "teams", force: :cascade do |t|
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.string "team_name"
end
create_table "users", force: :cascade do |t|
t.string "email", default: "", null: false
t.string "encrypted_password", default: "", null: false
t.string "reset_password_token"
t.datetime "reset_password_sent_at"
t.datetime "remember_created_at"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.string "confirmation_token"
t.datetime "confirmed_at"
t.datetime "confirmation_sent_at"
t.string "firstname"
t.integer "team_id"
t.index ["confirmation_token"], name: "index_users_on_confirmation_token", unique: true
t.index ["email"], name: "index_users_on_email", unique: true
t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true
end
team_controller.rb
class TeamController < ApplicationController
before_action :authenticate_user!
def index
#team = current_user.team
end
def new_team
end
def create_team
#team = current_user.create_team(sanitize_team)
if #team.save
redirect_to team_root_path
else
render json: #team.errors.full_messages
end
end
def join_team
#teams = Team.all
end
def team
end
private
def sanitize_team
params.require(:team).permit(:team_name, :team_statement)
end
end
I want the users 'team_id' attribute to update with the teams id when they create a team. or when they join a team. Are my associations correct? how would i make this happen in the controller ?

Yes, associations are correct. You can do it better only by adding foreign key to your database schema. It can be done by generator rails g migration AddTeamToUsers team:references
More information about associations can be found here: https://guides.rubyonrails.org/association_basics.html
In controller you have to change only the whitelisting params to allow team_id. And you probably need to add to your form in view something like this:
<%= f.select :team_id, Team.all.map { |t| [t.team_name, t.id] } %>

Let's strip your code example down to the minimum required:
# app/models/team.rb
class Team < ApplicationRecord
has_many :users
end
# app/models/user.rb
class User < ApplicationRecord
belongs_to :team
end
# db/migrate/20181124230131_create_teams.rb
class CreateTeams < ActiveRecord::Migration[5.2]
def change
create_table :teams do |t|
t.string :team_name
t.timestamps
end
end
end
# db/migrate/20181124230136_create_users.rb
class CreateUsers < ActiveRecord::Migration[5.2]
def change
create_table :users do |t|
t.belongs_to :team
t.timestamps
end
end
end
Then in your controller:
team = Team.where(team_name: 'foo').first_or_create!
team.users << current_user

Start by setting the association up as optional:
class User < ApplicationController
belongs_to :team, optional: true
end
Otherwise the validations on the user model will not let the user be saved without a team.
Then setup the teams resource:
# config/routes.rb
resources :teams do
post :join
end
post :join creates an additional POST /teams/:team_id/join route.
Then setup the controller:
class TeamsController
# ...
# GET /teams/new
def new
#team = Team.find
end
# POST /teams
def create
#team = Team.new(team_params)
if #team.save
unless current_user.team
current_user.update(team: #team)
end
redirect_to 'somewhere'
else
render :new
end
end
# ...
def join
#team = Team.find(params[:team_id])
if current_user.update(team: #team)
redirect_to #team, notice: 'Team joined'
else
redirect_to #team, error: 'Could not join team'
end
end
#
private
def team_params
params.require(:team).permit(:team_name, :team_statement)
end
end
Note that prefixing your action names is neither needed nor compatible with the "Rails way". Prefixing column names is also largely superfluous.

Related

Adding content from one controller to another controller's index

My goal is for users to add individual games pulled from an API gem (https://github.com/games-directory/api-giantbomb) to their personal library. I want users to be able to browse other people's libraries. I have the games showing up via search along with a show page for each game.
I am running into two problems: can't add games to a user's library and can't view other people's library.
Here is my games controller:
class GamesController < ApplicationController
#search for games
def index
#games = GiantBomb::Search.new().query(params[:query]).resources('game').limit(100).fetch
end
#Shows data for individual games
def show
#game = GiantBomb::Game.detail(params[:id])
end
#Adding and removing games to a user's library
def library
type = params[:type]
#game = GiantBomb::Game
if type == "add"
current_user.library_additions << #game
redirect_to user_library_path, notice: "Game was added to your library"
elsif type == "remove"
current_user.library_additions.delete(#game)
redirect_to root_path, notice: "Game was removed from your library"
else
# Type missing, nothing happens
redirect_to game_path(#game), notice: "Looks like nothing happened. Try once more!"
end
end
private
def game_params
params.require(:game).permit(:name, :search, :query)
end
end
When I try to add a game to my library, I get "Game(#70231217467720) expected, got GiantBomb::Game which is an instance of Class(#70231150447440)". So my #game is incorrect but I am not sure what should be there instead.
Even if I could add the game to my library, I can't view other user's libraries. Here is my current controller.
class LibraryController < ApplicationController
#before_action :authenticate_user!
def index
#library_games = User.library_additions
end
end
I get 'undefined method library_additions' even though it is in the model. If I change User to current_user I can see the page, but that means users can only see their page and not others.
Here are my game, user, and library model:
class Game < ApplicationRecord
has_many :libraries
has_many :added_games, through: :libraries, source: :user
end
class User < ApplicationRecord
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :validatable
has_many :games
has_many :libraries
has_many :library_additions, through: :libraries, source: :game
end
class Library < ApplicationRecord
belongs_to :game
belongs_to :user
end
I made my library a join table for users and games but I am thinking I didn't do it correctly. Here is my schema:
ActiveRecord::Schema.define(version: 2020_11_19_143536) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
create_table "games", force: :cascade do |t|
t.string "name"
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
t.string "search"
end
create_table "libraries", force: :cascade do |t|
t.integer "user_id"
t.integer "game_id"
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 "email", default: "", null: false
t.string "encrypted_password", default: "", null: false
t.string "reset_password_token"
t.datetime "reset_password_sent_at"
t.datetime "remember_created_at"
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
t.index ["email"], name: "index_users_on_email", unique: true
t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true
end
end
Am I missing a migration or do I need the rework the models and controllers?
[edit] Here are my routes, I am getting a pathing error when I try to add a game.
Rails.application.routes.draw do
devise_for :users
resources :games do
member do
put "add", to: "games#library"
put "remove", to: "games#library"
end
end
resources :library, only:[:index]
root to: 'pages#home'
get '/search', to: 'games#search', as: :search
get '/games', to: 'games#index', as: :index
get '/user/:id', to: 'user#show'
get '/user/:id/library', to: 'library#index', as: :user_library
end
Here, the error clearly states it is expecting an instance of Game not GiantBomb::Game, so you have to create one.
#game = Game.new(name: 'some name', other fields ....)
if type == "add"
current_user.library_additions << #game
About the other error you can only call association methods on an instance not on the class itself
def index
# You could get the user'id through params for example
#library_games = User.find(params[:user_id]).library_additions
end

Can't add attribute from another model

I have two models: Account and Profile.
I want render json API with attributes of Profile and include there one attribute from Account.
profile_serializer.rb
class ProfileSerializer < ActiveModel::Serializer
attributes :first_name, :middle_name, :last_name, :role
def role
#UserAccountSerializer.new(object.role)
object.account.role
end
end
account_serializer.rb
class UserAccountSerializer < ActiveModel::Serializer
attributes :role
end
profile.rb
class Profile < Grape::API
desc 'Current user profile'
get '/', serializer: ProfileSerializer do
current_user.profile
end
end
accounts_controller.rb
class AccountsController < AdminController
def index
#accounts = Account.all
end
def show
render json: #account
end
def update
#account = Account.find(params[:id])
redirect_to accounts_path if #account.update(role: params[:role])
end
end
account.rb
class Account < ApplicationRecord
belongs_to :profile
enum role: %i[user admin]
after_initialize :set_default_role, if: :new_record?
def set_default_role
self.role ||= :user
end
end
profile.rb
class Profile < ApplicationRecord
has_one :account
end
schema.rb
create_table "accounts", force: :cascade do |t|
t.string "email", default: "", null: false
t.bigint "profile_id", null: false
t.integer "role", default: 0
t.index ["email"], name: "index_accounts_on_email", unique: true
t.index ["profile_id"], name: "index_accounts_on_profile_id"
end
create_table "profiles", force: :cascade do |t|
t.string "last_name", null: false
t.string "first_name", null: false
t.string "middle_name"
end
Grape give me this error: undefined method `role' for nil:NilClass

After being saved the foreign_key = nil - Solving error in db

I save the #booking with a user (called "booker"). Right after the #booking.save I can retrieve #booking.booker in the command line that display all the properties from the user (email, password, id, etc.). However After leaving the create method, impossible to retrieve it (for example from the show) : #booking.booker = nil .
I guess that commes from a mistake in my booking model : I have belongs_to and has_many_through. If the error comes from here, how to solve it without having to change all the db?
booking_controller.rb
class BookingsController < ApplicationController
before_action :set_booking, only: [:show, :edit, :update ]
before_action :set_booking_format, only: [:destroy ]
def index
end
def my_bookings
#bookings = BookingPolicy::Scope.new(current_user, Booking).scope.where(booker: current_user)
end
def show
authorize #booking
end
def new
#garden = Garden.find(params[:garden_id])
#booking = Booking.new
authorize #booking
end
def create
#garden = Garden.find params[:garden_id]
#booking = #garden.bookings.build(booker: current_user)
authorize #booking
if #booking.save
redirect_to garden_booking_path(#booking, current_user)
end
end
def update
end
private
def set_booking
#booking = Booking.find(params[:id])
end
def set_booking_format
#booking = Booking.find(params[:format])
end
def booking_params
params.require(:booking).permit(:garden_id, :booker_id, :date)
end
end
booking.rb
class Booking < ApplicationRecord
belongs_to :garden
belongs_to :booker, class_name: "User"
end
garden.rb
class Garden < ApplicationRecord
belongs_to :user
has_many :bookings, dependent: :destroy
end
user.rb
class User < ApplicationRecord
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :validatable
has_many :gardens
has_and_belongs_to_many :bookings
end
schema.rb
create_table "bookings", force: :cascade do |t|
t.date "date"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.bigint "garden_id"
t.integer "booker_id"
t.index ["garden_id"], name: "index_bookings_on_garden_id"
end
create_table "gardens", force: :cascade do |t|
t.string "title"
t.text "details"
t.integer "surface"
t.text "address"
t.bigint "user_id"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.text "availabilities"
end
create_table "users", force: :cascade do |t|
t.string "email", default: "", null: false
t.string "encrypted_password", default: "", null: false
t.string "reset_password_token"
t.datetime "reset_password_sent_at"
t.datetime "remember_created_at"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.boolean "admin", default: false
t.index ["email"], name: "index_users_on_email", unique: true
t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true
end
add_foreign_key "bookings", "gardens"
end
In your model, user.rb:
class User < ApplicationRecord
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :validatable
has_many :gardens
has_and_belongs_to_many :bookings
end
The :bookings association should be has_many. You aren't using a join table.
See: https://guides.rubyonrails.org/association_basics.html#the-has-and-belongs-to-many-association
The belongs_to part of the habtm association is looking for a foreign key, which doesn't exist. You can retrieve #booking.booker before moving to a different controller action because you aren't hitting the database at all, you're just retrieving the instance variables' association.

How to fill multiple form fields using attribute from another model?

I have 3 models - User, Shipment and Friendship. User can be friends with another user via Friendship-model. User also can create Shipments and can add a Friend-User to it. There is address-attribute in User and Shipment models. I need to give User a possibility to fill that address field in 2 ways at the same form:
By filling the address field manually.
By choosing from select-list a Friend of that User - so the Friends
address-attribute transfers and fills the Shipments adress-attribute
(like ctrl-c/ctrl-v) and User can Submit the form.
I can guess, that AJAX is needed to refresh the content without refreshing the page.
Shipment model:
class Shipment < ActiveRecord::Base
belongs_to :user
belongs_to :friendship
validates :image, presence: true
validates :user_id, presence: true
end
Shipments controller:
class ShipmentsController < ApplicationController
helper_method :shipment, :user
before_action :set_shipment, only: [:show]
before_action :authenticate_user!
before_action :require_same_user, only: [:show]
def index
#shipments = Shipment.all
end
def new
#shipment = Shipment.new
end
def create
#shipment = Shipment.new(shipment_params)
#shipment.user = current_user
if #shipment.save
flash[:success] = "Shipment etc."
redirect_to shipment_path(#shipment)
else
render 'new'
end
end
def show
#shipment = Shipment.find(params[:id])
end
private
def user
#user = current_user
end
def shipment
#shipment = user.shipments.new
end
def shipment_params
params.require(:shipment).permit(:name, :kg, :length, :width, :height,
:adress, :image, :user_id, :friend_id)
end
def set_shipment
#shipment = Shipment.find(params[:id])
end
def require_same_user
if current_user != #shipment.user
flash[:alert] = "Restricted/"
redirect_to root_path
end
end
end
User model:
class User < ActiveRecord::Base
# Include default devise modules. Others available are:
# :confirmable, :lockable, :timeoutable and :omniauthable
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :trackable, :validatable
has_many :shipments, dependent: :destroy
has_many :friendships
has_many :friends, through: :friendships
has_many :inverse_friendships, :class_name => 'Friendship',
:foreign_key => 'friend_id'
has_many :inverse_friends, :through => :inverse_friendships, :source => :user
end
Users controller (the User itself is created by Devise)
class UsersController < ApplicationController
before_action :authenticate_user!
def show
#user = User.find(params[:id])
end
def my_friends
#friendships = current_user.friends
end
def search
#users = User.search(params[:search_param])
if #users
#users = current_user.except_current_user(#users)
render partial: 'friends/lookup'
else
render status: :not_found, nothing: true
end
end
private
def require_same_user
if current_user != set_user
flash[:alert] = "Restricted."
redirect_to root_path
end
end
def set_user
#user = User.find(params[:id])
end
end
Friendship model:
class Friendship < ActiveRecord::Base
belongs_to :user
belongs_to :friend, class_name: 'User'
has_many :shipments
end
Friendships controller:
class FriendshipsController < ApplicationController
def index
#friendships = Friendship.all
end
def create
#friendship = current_user.friendships.build(:friend_id => params[:friend_id])
if #friendship.save
flash[:success] = "Added to friends."
redirect_to my_friends_path
else
flash[:alert] = "Impossible to add as a friend."
redirect_to my_friends_path
end
end
def destroy
#friendship = current_user.friendships.find_by(friend_id: params[:id])
#friendship.destroy
flash[:notice] = "Unfriended."
redirect_to my_friends_path
end
private
def name
#name = friend_id.name
end
end
Schema:
create_table "friendships", force: :cascade do |t|
t.integer "user_id"
t.integer "friend_id"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
create_table "shipments", force: :cascade do |t|
t.string "name"
t.integer "length"
t.integer "width"
t.text "adress"
t.integer "user_id"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.string "image_file_name"
t.string "image_content_type"
t.integer "image_file_size"
t.datetime "image_updated_at"
t.integer "height"
t.integer "kg"
end
add_index "shipments", ["user_id"], name: "index_shipments_on_user_id"
create_table "users", force: :cascade do |t|
t.string "email", default: "", null: false
t.string "encrypted_password", default: "", null: false
t.string "reset_password_token"
t.datetime "reset_password_sent_at"
t.datetime "remember_created_at"
t.integer "sign_in_count", default: 0, null: false
t.datetime "current_sign_in_at"
t.datetime "last_sign_in_at"
t.string "current_sign_in_ip"
t.string "last_sign_in_ip"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.string "name"
t.integer "phone", limit: 30
t.string "username"
end
add_index "users", ["email"], name: "index_users_on_email", unique: true
add_index "users", ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true
Shipment form view (new):
<%= form_for(shipment, html: { multipart: true }) do |f| %>
<p>Choose a friend from your friendlist or fill the address field manually:</p>
<%= f.select :friend_id, user.friendships.map{ |friendship|
[friendship.friend.name, friendship.id] } %>
<%= f.text_field :adress, placeholder: "Address and index" %>
<%= f.submit "Submit", class: "button" %>
<% end %>
With ActiveRecord::Base, you could use eager loading and nested form to solve your problem.
Eager load the object related to the main object and use nested form to display the related object.

User_id is nil on rails association

I have a user model and a shout model. I am trying to have a user be associated with a shout. I did not make this association upon creation of the shouts table so I had to run a new migration. Below is my table, the models of each, and the output when from my console I run a command to try and find the user_id of a shout. Can you see what I am doing wrong?
schema:
create_table "shouts", force: :cascade do |t|
t.string "title"
t.text "description"
t.integer "user_id"
end
add_index "shouts", ["user_id"], name: "index_shouts_on_user_id"
create_table "users", force: :cascade do |t|
t.string "email", null: false
t.string "password_digest", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.string "username"
end
User Model:
class User < ActiveRecord::Base
validates :email, presence: true, uniqueness: true
validates :password_digest, presence: true
has_many :shouts
end
Shout Model:
class Shout < ActiveRecord::Base
belongs_to :user
end
Shout Controller:
class ShoutsController < ApplicationController
def new
#new_shout = Shout.new
end
def create
#new_shout = Shout.new(shouts_params)
if #new_shout.user_id == nil
render new_shout_path
elsif #new_shout.save
redirect_to dashboard_path
else
render new_shout_path
end
end
private
def shouts_params
params.require(:shout).permit(:title, :description, :user_id)
end
end
Some test code:
> Shout.find(4)
> #<Shout id: 4, title: "four", description: "four", user_id: nil>
Creating an instance of user from the console, working:
> User.first.shouts.create(title: 'four', description: 'four')
>[["title", "four"], ["description", "four"], ["user_id", 1]
Migration file:
class AddUserRefToShouts < ActiveRecord::Migration
def change
add_reference :shouts, :user, index: true, foreign_key: true
end
end
Here are a couple admittedly hacky options (but they'll work) if you don't want to follow the approach suggested in the comments. You could pass the user_id as a hidden field so it'll be included in the params or you can expressly set it in the create action.
If you want to pass as a hidden field, on your shout form add:
<%= f.hidden_field :user_id, value: current_user.id %>
Alternatively, if you want to handle in your create action:
def create
#shout = Shout.new(shout_params)
#shout.user_id = current_user.id
#shout.save
end

Resources