Im learning Rails and I'm just wondering if some code I wrote is correct and safe. I have two models, a user and post model. The posts belong to users, so I want to pass the user_id automatically to post when the object is created. I used an assign_attributes method in the post controller to set the user_id using the current_user helper provided by devise. Below is my relevant code. Again I want to know if this is correct or if there is better way of doing it.
def create
#post = Post.new(params[:post])
#post.assign_attributes({:user_id => current_user.id})
end
Post Model
class Post < ActiveRecord::Base
attr_accessible :content, :title, :user_id
validates :content, :title, :presence => true
belongs_to :user
end
User model
class User < ActiveRecord::Base
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :trackable, :validatable
# Setup accessible (or protected) attributes for your model
attr_accessible :email, :password, :password_confirmation, :remember_me
has_many :posts
end
You're pretty close. Since you've 1) been provided the current_user convenience helper by Devise, and 2) configured User and Post as a has_many/belongs_to relationship, it makes sense to create the new post, then append it to current_user. Then, in your Post model, you'll want to break up validations for individual attributes – the way you've listed :content, :title in sequence won't work.
# app/controllers/posts_controller.rb
def create
post = Post.create(params[:post])
current_user.posts << post
end
# app/models/post.rb
class Post < ActiveRecord::Base
attr_accessible :content, :title, :user_id
validates :content, :presence => true
validates :title, :presence => true
belongs_to :user
end
I don't think that is necessary since you have already created the relationship between posts and users. If you nest the posts resources into the user, it will automatically create the relationship between the 2 models.
In routes.rb
resources :users do
resources :posts
end
With that done, you will now reference posts as #user.post. I have already shown an example in this question.
I would say something like this :
def create
params[:post][:user_id] = current_user.id
#post = Post.new(params[:post])
#post.save
end
or
def create
#post = Post.new(params[:post])
#post.user = current_user
if #post.save
...
else
...
end
end
or
def create
#post = #post.new(params[:post])
#post.user_id = current_user.id
#post.save
end
You could put the user_id in the params but that would not be safe. user_id should not be in 'attr_accessable' so it will be protected for mass_assignment.
Related
I'm trying to learn Ruby on Rails, an I'm kinda stuck with associaton.
My project is to create a simple blog with three table. User, Post, and Comment.
In my understanding, after associationg several table with foreign key, rails would automatcily find user_id and post_id. But everytime I try to build comments, the user_id is nil.
Here's my model:
class User < ApplicationRecord
has_many :posts
has_many :comments
validates :name, presence: true, length: { minimum: 5 }, uniqueness: true
validates :password, presence: true, length: { minimum: 5 }
end
class Post < ApplicationRecord
belongs_to :user
has_many :comments
validates :title, presence: true
validates :body, presence: true, length: {minimum: 10}
end
class Comment < ApplicationRecord
belongs_to :post
belongs_to :user
validates :body, presence: true
validates :user_id, presence: true
validates :post_id, presence: true
end
Here is the screenshot when I try to create a comment:
As you can see, the post_id is not nil but the user_id is nil.
I try to input user_id manualy and it work as intended. But I can't find out how to create comment with automatic user_id and post_id.
In my understanding, after associationg several table with foreign key, rails would automatcily find user_id and post_id. But everytime I try to build comments, the user_id is nil.
There is no truth to that assumption. Rails will not automatically assign your assocations - how should it even know what user/post you want to associate the comment with?
Typically the way you would construct this is to have a nested route:
resources :posts do
resources :comments,
only: [:create]
shallow: true
end
This creates the route /posts/:post_id/comments so that we know which post the user wants to comment on - you would then adjust your forms so that it posts to the nested route:
# app/views/comments/_form.html.erb
<%= form_with(model: [post, comment]) do |f| %>
# ...
<% end %>
# app/views/posts/show.html.erb
# ....
<h2>Leave a comment</h2>
<%= render partial: 'comments/form',
locals: {
post: #post,
comment: #comment || #post.comments.new
}
%>
Getting the user who's commenting would typically be done by getting it from the session through your authentication system - in this example the authenticate_user! callback from Devise would authenticate the user and otherwise redirect to the sign in if no user is signed in.
You then simply assign the whitelisted parameters from the request body (from the form) and the user from the session:
class CommentsController
before_action :authenticate_user!
# POST /posts/1/comments
def create
# This gets the post from our nested route
#post = Post.find(params[:post_id])
#comment = #post.comments.new(comment_params) do |c|
c.user = current_user
end
if #comment.save
redirect_to #post,
status: :created
notice: 'Comment created'
else
render 'comments/show',
status: :unprocessable_entity,
notice: 'Could not create comment'
end
end
private
def comment_params
params.require(:comment)
.permit(:foo, :bar, :baz)
end
end
This is typically the part that Rails beginners struggle the most with in "Blorgh" tutorials as it introduces a resource thats "embedded" in another resource and its views and several advanced concepts. If you haven't already I read it would really recommend the Getting Started With Rails Guide.
you can create a comments as below:
user = User.find 2
post = user.posts.where(id: 2).first
comment = post.comments.build({comment_params}.merge(user_id: user.id))
Hope this will help you.
First this is all of my code
#models/user.rb
class User < ApplicationRecord
has_many :trips
has_many :homes, through: :trips
has_secure_password
accepts_nested_attributes_for :trips
accepts_nested_attributes_for :homes
validates :name, presence: true
validates :email, presence: true
validates :email, uniqueness: true
validates :password, presence: true
validates :password, confirmation: { case_sensitive: true }
end
#home.rb
class Home < ApplicationRecord
has_many :trips
has_many :users, through: :trips
validates :address, presence: true
end
class HomesController < ApplicationController
def show
#home = Home.find(params[:id])
end
def new
if params[:user_id]
#user = User.find_by(id: params[:user_id])
#home = #user.homes.build
end
end
def create
#user = User.find_by(id: params[:user_id])
binding.pry
#home = Home.new
end
private
def home_params
params.require(:home).permit(:address, :user_id)
end
end
I am trying to do something like this so that the home created is associated with the user that is creating it.
def create
#user = User.find_by(id: params[:user_id])
#home = Home.new(home_params)
if #home.save
#user.homes << #home
else
render :new
end
end
The problem is that the :user_id is not being passed into the params. So the #user comes out as nil. I can't find the reason why. Does this example make sense? Am I trying to set the associations correctly? Help or any insight would really be appreciated. Thanks in advance.
The way you would typically create resources as the current user is with an authentication such as Devise - not by nesting the resource. Instead you get the current user in the controller through the authentication system and build the resource off it:
resources :homes
class HomesController < ApplicationController
...
# GET /homes/new
def new
#home = current_user.homes.new
end
# POST /homes
def create
#home = current_user.homes.new(home_parameters)
if #home.save
redirect_to #home
else
render :new
end
end
...
end
This sets the user_id on the model (the Trip join model in this case) from the session or something like an access token when dealing with API's.
The reason you don't want to nest the resource when you're creating them as a specific user is that its trivial to pass another users id to create resources as another user. A session cookie is encrypted and thus much harder to tamper with and the same goes for authentication tokens.
by using if params[:user_id] and User.find_by(id: params[:user_id]) you are really just giving yourself potential nil errors and shooting yourself in the foot. If an action requires a user to be logged use a before_action callback to ensure they are authenticated and raise an error and bail (redirect the user to the sign in). Thats how authentication gems like Devise, Knock and Sorcery handle it.
I'm using has_many through relationship and I don't really get what should I do else to make it work.
I suppose there is something about parameters that I don't understand and omit. If so, please tell me where and how to write it, because I'm confused a little bit because of all these params.
book.rb:
class Book < ActiveRecord::Base
has_many :user_books
has_many :users, through: :user_books
end
user.rb:
class User < ActiveRecord::Base
devise :database_authenticatable, :registerable, :confirmable,
:recoverable, :rememberable, :trackable, :validatable
validates :full_name, presence: true
has_many :user_books
has_many :books, through: :user_books #books_users - book_id user_id
end
and books_controller.rb:
class BooksController < ApplicationController
before_action :is_admin?, except: [:show_my_books, :book_params]
before_filter :authenticate_user!
expose(:book, attributes: :book_params)
expose(:user_book)
expose(:user_books)
expose(:books){current_user.books}
def create
if book.save
redirect_to(book)
else
render :new
end
end
def update
if book.save
redirect_to(book)
else
render :edit
end
end
def show
end
def is_admin?
if current_user.admin?
true
else
render :text => 'Who are you to doing this? :)'
end
end
def book_params
params.require(:book).permit(:name, :author, :anotation, user:[:name])
end
end
When I create new book it gives me an error
Couldn't find Book with 'id'=27 [WHERE "user_books"."user_id" = ?]
<%=book.name%>
Sorry for a silly question, but I couldn't find a proper example to understand it myself that's why I ask you for help. Every help would be appreciated, thank you!
To setup a relation via a form you usually use a select or checkbox and pass the ID(s) of related item(s):
For a one to one relation the request would look like this:
POST /books { book: { name: 'Siddharta', author: 'Herman Hesse', user_id: 1 } }
For many to many or one to many you can use _ids:
POST /books { book: { name: 'Siddharta', author: 'Herman Hesse', user_ids: [1,2,3] } }
ActiveRecord creates a special relation_name_ids= setter and getter for has_many and HABTM relations. It lets you modify the relations of an object by passing an array of IDs.
You can create the form inputs like so:
<%= form_for(#book) do |f| %>
<%= f.collection_select(:author_ids, User.all, :id, :name, multiple: true) %>
OR
<%= f.collection_checkboxes(:author_ids, User.all, :id, :name) %>
<% end %>
To whitelist the user_ids params which should permit an array of scalar values and not a nested hash we pass an empty array:
def book_params
params.require(:book).permit(:name, :author, :anotation, user_ids: [])
end
On the other hand if you want to assign records to the current user it is better to get the user from the session or a token and avoid passing the param at all:
def create
#book = current_user.books.new(book_params)
# ...
end
This lets you avoid a pretty simple hack where a malicious user passes another users id or takes control of a resource by passing his own id.
As to your other error why it would try to create a strange query some sort of stack trace or log is needed.
However if you are new to Rails you might want to hold off a bit on the decent exposure gem. It obscures away a lot of important concepts in "magic" - and you'll spend more time figuring out how it works that might be better spent learning how good rails apps are built.
There are different kinds of users in my system. One kind is, let's say, a designer:
class Designer < ActiveRecord::Base
attr_accessible :user_id, :portfolio_id, :some_designer_specific_field
belongs_to :user
belongs_to :portfolio
end
That is created immediately when the user signs up. So when a user fills out the sign_up form, a Devise User is created along with this Designer object with its user_id set to the new User that was created. It's easy enough if I have access to the code of the controller. But with Devise, I don't have access to this registration controller.
What's the proper way to create a User and Designer upon registration?
In a recent project I've used the form object pattern to create both a Devise user and a company in one step. This involves bypassing Devise's RegistrationsController and creating your own SignupsController.
# config/routes.rb
# Signups
get 'signup' => 'signups#new', as: :new_signup
post 'signup' => 'signups#create', as: :signups
# app/controllers/signups_controller.rb
class SignupsController < ApplicationController
def new
#signup = Signup.new
end
def create
#signup = Signup.new(params[:signup])
if #signup.save
sign_in #signup.user
redirect_to projects_path, notice: 'You signed up successfully.'
else
render action: :new
end
end
end
The referenced signup model is defined as a form object.
# app/models/signup.rb
# The signup class is a form object class that helps with
# creating a user, account and project all in one step and form
class Signup
# Available in Rails 4
include ActiveModel::Model
attr_reader :user
attr_reader :account
attr_reader :membership
attr_accessor :name
attr_accessor :company_name
attr_accessor :email
attr_accessor :password
validates :name, :company_name, :email, :password, presence: true
def save
# Validate signup object
return false unless valid?
delegate_attributes_for_user
delegate_attributes_for_account
delegate_errors_for_user unless #user.valid?
delegate_errors_for_account unless #account.valid?
# Have any errors been added by validating user and account?
if !errors.any?
persist!
true
else
false
end
end
private
def delegate_attributes_for_user
#user = User.new do |user|
user.name = name
user.email = email
user.password = password
user.password_confirmation = password
end
end
def delegate_attributes_for_account
#account = Account.new do |account|
account.name = company_name
end
end
def delegate_errors_for_user
errors.add(:name, #user.errors[:name].first) if #user.errors[:name].present?
errors.add(:email, #user.errors[:email].first) if #user.errors[:email].present?
errors.add(:password, #user.errors[:password].first) if #user.errors[:password].present?
end
def delegate_errors_for_account
errors.add(:company_name, #account.errors[:name].first) if #account.errors[:name].present?
end
def persist!
#user.save!
#account.save!
create_admin_membership
end
def create_admin_membership
#membership = Membership.create! do |membership|
membership.user = #user
membership.account = #account
membership.admin = true
end
end
end
An excellent read on form objects (and source for my work) is this CodeClimate blog post on Refactoring.
In all, I prefer this approach vastly over using accepts_nested_attributes_for, though there might be even greater ways out there. Let me know if you find one!
===
Edit: Added the referenced models and their associations for better understanding.
class User < ActiveRecord::Base
# Memberships and accounts
has_many :memberships
has_many :accounts, through: :memberships
end
class Membership < ActiveRecord::Base
belongs_to :user
belongs_to :account
end
class Account < ActiveRecord::Base
# Memberships and members
has_many :memberships, dependent: :destroy
has_many :users, through: :memberships
has_many :admins, through: :memberships,
source: :user,
conditions: { 'memberships.admin' => true }
has_many :non_admins, through: :memberships,
source: :user,
conditions: { 'memberships.admin' => false }
end
This structure in the model is modeled alongside saucy, a gem by thoughtbot. The source is not on Github AFAIK, but can extract it from the gem. I've been learning a lot by remodeling it.
If you don't want to change the registration controller, one way is to use the ActiveRecord callbacks
class User < ActiveRecord::Base
after_create :create_designer
private
def create_designer
Designer.create(user_id: self.id)
end
end
I have a User model (generated by devise) and a Submission model in a Rails project. I've added a field called 'full_name' to the user model. Also, there is a field for 'user_id' in the submission model.
I want to show the the full_name of the user associated with the submission on the submission show page. Right now it shows the user_id from the submission model just fine.
The User model I have this:
class User < ActiveRecord::Base
has_many :submissions
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :trackable, :validatable, :confirmable
attr_accessible :email, :password, :password_confirmation, :remember_me, :full_name, :role_id
validates_presence_of :full_name
end
This is the model for submissions:
class Submission < ActiveRecord::Base
belongs_to :user
attr_accessible :description, :title, :user_id
end
This is in the controller:
def show
#submission = Submission.find(params[:id])
#user = User.find(params[#submission.user_id])
respond_to do |format|
format.html # show.html.erb
format.json { render json: #submission }
end
end
What I get when I try to use this line in the view:
<%= #user.full_name %>
I get this error:
Couldn't find User without an ID
I've tried several variations and can't quite figure out what should be in the place of:
#user = User.find(params[#submission.user_id])
Assuming that you're populating the data correctly, you only need the following once you have looked up the submission.
#submission.user
If you think that the data is OK, then try the following in the Rails console.
> Submission.first.user
What do you see there?
The specific error that you are seeing is because this:
params[#submission.user_id]
is unlikely to ever have anything in it. If the user ID is "1" (for example) then you're asking for the value in the params hash that corresponds to the key "1".