User and Link association difficulties - ruby-on-rails

I have a user and link MVC. I listed them below. Essentially when I create a new link I want it to be tied to my user and then display the users email with the quote on my show page, but I continue to get a nil value for a user when I authenticate even though I have:
a user and link assocation
permitted :user_id in my strong parameters
have a before_filter that requires a user be logged in when making a new request
have a user_id in my link schema
If you take a look at my show.html.erb and the line <%= #link.user.try(:email) %>, this is where the users email should appear that posted the link, but they all come across as nil values.
I am a little lost right now as to why I can't get this to work, any help would be very appreciated!
Models:
class User < ActiveRecord::Base
has_many :links
acts_as_voter
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :trackable, :validatable
end
class Link < ActiveRecord::Base
belongs_to :user
acts_as_votable
attr_accessor :avatar
mount_uploader :avatar, AvatarUploader
end
Controllers:
class LinksController < ApplicationController
before_filter :authenticate_user!, except: [:index, :show]
def index
#links = Link.all
end
def show
#link = Link.find(params[:id])
end
def new
#link = Link.new
end
def edit
end
def create
#link = Link.new(link_params)
if #link.save
redirect_to root_path
else
render 'new'
end
end
private
def link_params
params.require(:link).permit(:title, :url, :avatar, :user_id)
end
end
show.html.erb:
<p id="notice"><%= notice %></p>
<p>
<strong>Author:</strong>
<%= #link.title %>
</p>
<p>
<strong>Quote:</strong>
<%= #link.url %>
</p>
<small class="author">Submitted <%= time_ago_in_words(#link.created_at) %> ago by <%= #link.user.try(:email) %></small>
Schema:
create_table "links", force: :cascade do |t|
t.string "title"
t.string "url"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.integer "user_id"
t.integer "cached_votes_total", default: 0
t.integer "cached_votes_score", default: 0
t.integer "cached_votes_up", default: 0
t.integer "cached_votes_down", default: 0
t.string "avatar"
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.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
end

It doesn't sound like user_id is ever set. As it happens, you probably don't want to set it via the params anyway (since these can be manipulated by the user).
Instead, replace
#link = Link.new(link_params)
With
#link = current_user.links.build(link_params)
If links always have a user, I would also mark the user_id column as not null instead of littering my app with calls to try (And I would in general prefer try! to try)

1.As I understand, the logic is that you can only create links once your logged in. And a user can create links only for him/her (only user logged in can be the owner of links he created). In that case you should not expose user_id in form but instead add it in the create action:
def create
#link = Link.new(link_params)
if #link.save
current_user.links << #link
redirect_to root_path
else
render 'new'
end
end
2.Also, if you don't allow orphan links(with no user as the owner), then I suggest not using user.try(:email) but instead #link.user.email
If you are accessing user object through #link then you could preload it in the show action to save some DB queries
def show
#link = Link.includes(:user).find(params[:id])
end
Make these changes and try again. Also, email is required for a user if you are using default devise configuration, so .try(:email) returning nil might mean that the user object is not found, which means that the associations are not properly set (my version might fix that)

Related

Rails 6 : How to associate a user to a created post when post is created through web scraping controller action

This question is a bit complicated to ask and probably more difficult to understand so I will do my best to add context to explain my goal.
My app allows logged-in users to run a web scraping function on a car dealership's website and stores it's inventory to a database. I would like to associate the current_user.id of who ran the scraping function to the posts created by it.
First, I generated a migration to associate "vehicles" with "users" by running
rails g migration AddUserRefToVehicles user:references
then, I added a belongs_to :user association to the Vehicle model
then, I updated the create action in VehiclesController
The VehiclesController calls on a "scrape" function which runs the "vehicles_spider.rb" model and creates the posts and saves it to a database.
vehicles_controller.rb :
class VehiclesController < ApplicationController
before_action :authenticate_user!
before_action :set_vehicle, only: %i[ show edit update destroy ]
...
#Calls vehicles_spider.rb
def scrape
url = 'https://www.example.com/vehicles'
response = VehiclesSpider.process(url)
if response[:status] == :completed && response[:error].nil?
flash.now[:notice] = "Successfully scraped url"
else
flash.now[:alert] = response[:error]
end
rescue StandardError => e
flash.now[:alert] = "Error: #{e}"
end
def new
#vehicle = Vehicle.new
end
def create
#This didn't work
#vehicle = Vehicle.new(vehicle_params.merge(user_id: current_user.id))
end
...
private
# Use callbacks to share common setup or constraints between actions.
def set_vehicle
#vehicle = Vehicle.find(params[:id])
end
# Only allow a list of trusted parameters through.
def vehicle_params
params.require(:vehicle).permit(:title, :stock_number, :exterior_color,
:interior_color, :transmission, :drivetrain, :price)
end
end
vehicle.rb
class Vehicle < ApplicationRecord
belongs_to :user
end
vehicles_spider.rb
class VehiclesSpider < Kimurai::Base
require 'nokogiri'
require 'httparty'
require 'byebug'
require 'watir'
#name = 'vehicles_spider'
#engine = :mechanize
def self.process(url)
#start_urls = [url]
self.crawl!
end
def parse(response, url:, data: {})
url = "https://www.example.com/new-vehicles/"
{ ... }
pagination_vehicle_listings.each do |vehicle_listing|
vehicle = {title: ...
price: ...
color: ...
}
// This line is what creates a post and saves the scraped data to my database.
Vehicle.where(vehicle).first_or_create
{ ... }
end
schema.rb
ActiveRecord::Schema.define(version: 2021_07_14_104441) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
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.string "name"
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
create_table "vehicles", force: :cascade do |t|
t.string "title"
t.string "stock_number"
t.string "exterior_color"
t.string "interior_color"
t.string "transmission"
t.string "drivetrain"
t.integer "price"
t.integer "miles"
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
t.bigint "user_id", null: false
t.index ["user_id"], name: "index_vehicles_on_user_id"
end
add_foreign_key "vehicles", "users"
end
The program was saving the posts just fine until I associated "vehicles" to "users" and now the scraped data is refusing to save. My hunch is that the vehicle posts that are being created by the "vehicle_spider.rb" model arn't being associated with the logged-in user.
If I understood the problem correctly, you should have access to the current_user object when running the scrape action.
You can pass that object to VehiclesSpider parse method and something like below to persist the data in the database.
current_user.vehicles.where(vehicle).first_or_create ...
I found a solution. The problem is rails models don't have access to the current_user object by default. We can give models access to the current_user object by adding the following to the Application Controller.
application_controller.rb
before_action :set_current_user
def set_current_user
Vehicle.current_user = current_user
end
added this to vehicle model
vehicle.rb
cattr_accessor :current_user
now that our models have has access to current_user I refactored this line from vehicles_spider.rb
Vehicle.where(vehicle).first_or_create
to
Vehicle.where(vehicle.merge(user_id: Vehicle.current_user.id)).first_or_create
and now the user ids are saved and associated with the posts!

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

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.

Unsure if association is working in my rails app

My app has users and users are able to post links to my app. I have an association set up so that a user has many :links and links belong_to a user (see below for models). Now I am trying to get the users email to appear in the show template and I am getting a nil value for Link.user for new links. Can someone maybe shed some light as to why? Is my association incorrect? The user has been logged in when posting links.
User model:
class User < ActiveRecord::Base
has_many :links
acts_as_voter
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :trackable, :validatable
end
Link model:
class Link < ActiveRecord::Base
belongs_to :user
acts_as_votable
attr_accessor :avatar
mount_uploader :avatar, AvatarUploader
end
show.html.erb:
<%= time_ago_in_words(#link.created_at) %> by <%= #link.user.try(:email) %>
Schema:
create_table "links", force: :cascade do |t|
t.string "title"
t.string "url"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.integer "user_id"
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.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
end
Link creation in Links controller;
def new
#link = Link.new
end
def create
#link = Link.new(link_params)
if #link.save
redirect_to root_path
else
render 'new'
end
end
private
def link_params
params.require(:link).permit(:title, :url, :avatar)
end
Make sure the links are actually being assigned to a user when created.
Using the rails console. Try looping through the links and making sure they have user_ids:
in the rails console:
ap Link.all.map(&:user)
If they are indeed owned by a user
<%= #link.user.email %>
should print out the email.
You should be able to do something like this in your controller:
#user.links << params[:new_link]
Make sure that much is working.
You are using devise. Inside your Link controller, you have to authenticate your logged on user.
# app/controllers/links_controller.rb
before_action :authenticate_user!
def create
#link = current_user.links.new(link_params)
if #link.save
redirect_to root_path
else
render 'new' # generally this is a render 'edit'
end
end
I truncated the file here. Please read the devise manual

Rails has_one and belongs_to help

I have two models: User and Store
class Store < ActiveRecord::Base
belongs_to :user
class User < ActiveRecord::Base
has_one :store
Schema looks like this:
create_table "users", :force => true do |t|
t.string "name"
t.string "email"
t.datetime "created_at"
t.datetime "updated_at"
t.string "encrypted_password"
t.string "salt"
t.boolean "admin", :default => false
t.string "username"
t.string "billing_id"
end
create_table "stores", :force => true do |t|
t.string "email"
t.datetime "created_at"
t.datetime "updated_at"
t.string "store_name"
t.integer "user_id"
end
User must login in order to sign up a store by inputting "email" and "store_name". create from stores_controller looks like this:
def create
#store = Store.new(params[:store])
if #store.save
#store.user_id = current_user.id
flash[:success] = "this store has been created"
redirect_to #store
else
#title = "store sign up"
render 'new'
end
end
In ApplicationsController
def current_user
#current_user ||= user_from_remember_token
end
However, when I check in the database, #store.user_id = nil. For some reason, it's not able to put in current_user.id into #store.user_id. Anybody able to help in detecting why this might be? I thought I had associations correctly implemented. Thanks
This is happening because you're setting the #store.user_id AFTER saving it.
Ideally, you should be using the association builder for this:
def new
#store = #current_user.store.build(params[:store])
More information about these can be found in the "Association Basics" guide.

Resources