Rails Devise error on building one to one relationship upon registration - ruby-on-rails

I want to create a one to one relationship, User -> Account upon Devise registration. I thought I had it figured out with the following code.
# account.rb
class Account < ActiveRecord::Base
belongs_to :user
end
# user.rb
class User < ActiveRecord::Base
has_one :account
end
# registrations_controller.rb
def create
super
current_user.build_account(account_params).save
end
And this code works about 80% of the time so far. But every once and a while, I get the following error.
undefined method 'build_account' for nil:NilClass
app/controllers/users/registrations_controller.rb:13:in 'create'
Clearly, the error is telling me, you cannot build_account when current_user is nil. That makes sense to me, but
Why does that happen only some of the time?
What is a more consistent way to build this one to one relationship upon Devise user registration?

When I do this I like to use nested forms.
# user.rb
class User < ActiveRecord::Base
belongs_to :account
accepts_nested_attributes_for :account
end
# account.rb
class Account < ActiveRecord::Base
belongs_to :user
end
#views registrations/new.html.haml
= simple_form_for(resource, :as => resource_name, :url => registration_path(resource_name)) do |f|
%ul
- resource.errors.full_messages.each do |e|
%li=e
= f.simple_fields_for :account do |account_form|
...
...
#Controller
class Users::RegistrationsController < Devise::RegistrationsController
def new
resource = build_resource({})
resource.build_account
respond_with resource
end
def create
build_resource(sign_up_params)
if resource.save
...
else
...
end
end
private
def sign_up_params
allow = [:first_name, :last_name, :phone, :email, :password, :password_confirmation, :agree_newsleter, account_attributes: [:name, :phone, :website]]
params.require(resource_name).permit(allow)
end
end

Related

Rails Client side Collection Validation fails - Simple Form

I have to build a simple app that allows users to loan and borrow books. Simply put a User can create books, and they can pick another user to loan the book to.
I have three models User, Book and Loan:
class User < ActiveRecord::Base
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :trackable, :validatable
has_many :books
has_many :loans, through: :books
has_many :borrowings, class_name: "Loan"
validates :username, uniqueness: true
validates :username, presence: true
end
class Book < ActiveRecord::Base
belongs_to :user
has_many :loans
validates :title, :author, presence: true
end
class Loan < ActiveRecord::Base
belongs_to :user
belongs_to :book
validates :user, :book, :status, presence: true
end
The LoansController looks like this:
class LoansController < ApplicationController
before_action :find_book, only: [:new, :create]
def new
#users = User.all
#loan = Loan.new
authorize #loan
end
def create
#loan = Loan.new
#loan.book = #book
#loan.user = User.find(loan_params[:user_id])
#loan.status = "loaned"
authorize #loan
if #loan.save
redirect_to :root
else
render :new
end
end
private
def loan_params
params.require(:loan).permit(:user_id)
end
def find_book
#book = Book.find(params[:book_id])
end
end
My form looks like:
<%= simple_form_for([#book, #loan]) do |f| %>
<%= f.input :user_id, collection: #users.map { |user| [user.username, user.id] }, prompt: "Select a User" %>
<%= f.submit %>
<% end %>
If I submit the form without selecting a user, and keep the "Select a User" prompt option, the form is submitted and the app crash because it can't find a user with id=
I don't know why the user presence validation in the form does not work...
you will change your Create method
def create
#loan = Loan.new
#loan.book = #book
#loan.user = User.find_by_id(loan_params[:user_id])
#loan.status = "loaned"
authorize #loan
if #loan.save
redirect_to :root
else
render :new
end
end

NoMethodError at / undefined method `name' for nil:NilClass

I have tried many solution and came up with good one but still getting error. I am editing my whole question.
I am trying to create Friendly URL with friendly_id gem.
In my project First user need to signup with devise.
Devise will pass some information to profile model with
model/user.rb
delegate :age, :city, :region, :country, to: :profile
I want to make user.name to be Friendly_id candidate. I have tried following code in my Profile model:-
class Profile < ActiveRecord::Base
extend FriendlyId
friendly_id :user_name , use: :slugged
def user_name
user.name
end
But it is giving error
NoMethodError at /
undefined method `name' for nil:NilClass
now After submitting user form.
Please suggest possible solution with explanation.
My User.rb looks like
require 'open-uri'
class User < ActiveRecord::Base
paginates_per 10
validates :name , presence: true, length: { maximum: 200 }
scope :by_name, ->(name) do
joins(:profile).where('lower(name) LIKE ?', "%#{name.downcase}%")
end
delegate :age, :city, :region, :country, to: :profile
has_one :profile, dependent: :destroy
accepts_nested_attributes_for :profile
def self.new_with_session(params, session)
session_params = { 'profile_attributes' => {} }
provider = session['devise.provider']
if provider && data = session["devise.#{provider}"]
session_params['name'] = data[:name] if data[:name]
session_params['email'] = data[:email] if data[:email]
session_params['profile_attributes'] =
{ avatar: data[:image] } if data[:image]
end
params.deep_merge!(session_params)
super.tap do |user|
if auth = Authorization.find_from_session(session, provider)
user.authorizations << auth
end
end
end
def password_required?
super && registered_manually?
end
def registered_manually?
encrypted_password.present?
end
end
And my profile.rb looks like
class Profile < ActiveRecord::Base
extend FriendlyId
friendly_id user.name, use: :slugged
belongs_to :user
accepts_nested_attributes_for :user
validates :website, allow_blank: true, uri: true
def website=(url_str)
if url_str.present?
url_str = "http://#{url_str}" unless url_str[/^https?/]
write_attribute :website, url_str
end
end
end
I think Problem is here:
Request parameters
{"action"=>"new", "controller"=>"users/registrations"}
Please suggest possible solution and explanation.
And users/registration:
class Users::RegistrationsController < Devise::RegistrationsController
layout 'land'
def create
params[:user][:profile_attributes].delete(:place)
end
protected
def after_sign_up_path_for(resource)
welcome_path
end
end
I am creating user in profile controller
def load_profile
#profile = Profile.friendly.find(params[:id])
if !#profile || #profile.user.blocked_users.include?(current_user)
redirect_to home_path
else
#user = #profile.user
end
end
#Rodrigo helped me find out error that error is due to Friendly_id can't create link with user instance.
There is an error on this line:
friendly_id user.name, use: :slugged
The variable user doesn't exists at Profile class scope. You should use something like this:
friendly_id :user_name, use: :slugged
def user_name
user.name
end
extend FriendlyId
friendly_id u_name, use: :slugged
def u_name
user.name
end
belongs_to :user
Have you defined user? what is user.name?

Comment author. Rails

I want show the user email as author of comment, but I see this error "undefined method `email' for nil:NilClass"
comment.rb
class Comment < ActiveRecord::Base
belongs_to :hotel
belongs_to :user
end
user.rb
class User < ActiveRecord::Base
has_many :hotels
has_many :comments
end
hotel.rb
class Hotel < ActiveRecord::Base
belongs_to :user
belongs_to :address
has_many :comments
mount_uploader :avatar, AvatarUploader
accepts_nested_attributes_for :address
end
comments_controller.rb
def create
#hotel = Hotel.find(params[:hotel_id])
#comment = #hotel.comments.new(comment_params)
#comment.user_id = current_user.id
#comment.save
redirect_to #hotel
end
private
def comment_params
params.require(:comment).permit(:user_id, :body, :hotel_id)
end
_comments.html.haml
= div_for comment do
%p
%strong
Posted #{time_ago_in_words(comment.created_at)} ago
%br/
= h comment.user.email
%br
= comment.body
Method
The error that you're calling a method which doesn't exist.
The problem is you're calling a method on an associated object which doesn't exist. You probably don't have any user associated to the comment - thus preventing you from being able to call the email method.
Firstly, you need to make sure you have the correct association. Here's how to do that:
$ rails c
$ comment = Comment.find([id])
$ comment.update(user_id: [your_user_id])
$ exit
This will allow you to associate the comment to a particular user, giving you the ability to call the associated method.
--
Controller
When you save your comment in your controller, you need to assign your user to it. We do this using the strong_params functionality, as its the DRYest way we've found:
#app/controllers/comments_controller.rb
Class CommentsController < ApplicationController
def create
#comment = Comment.new(comment_params)
end
private
def comment_params
params.require(:comment).permit(:your, :comment: attributes).merge(user_id: current_user.id)
end
end
This will allow you to associate the user at save time, giving you the ability to call the methods you need next time you call the record!
Delegate
You'll also benefit from using the delegate method like this:
#app/models/comment.rb
Class Comment < ActiveRecord::Base
belongs_to :user
belongs_to :hotel
delegate :email, to: :user, prefix: true #-> allows you to call `#comment.user_email`
end
This will solve the law of Demeter issue (where you should aim to have one "point" in your calls")

How to create another object when creating a Devise User from their registration form in Rails?

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

Rails & Rolify: Add default role on creation?

Currently I'm using Rolify & CanCan to manage roles and abilities in my Rails 3 app. My question is: How can I get a user to have a role by default on creation? for example, if I have a "user" role, ¿How can I make all the users that register in my app have a user Role by default? My Ability.rb has this code:
class Ability
include CanCan::Ability
def initialize(user)
user ||= User.new # guest user (not logged in)
if user.has_role? :admin
can :manage, :all
elsif user.has_role? :user
can :update, User, :id => user.id
end
end
end
My User Model has this one:
class User < ActiveRecord::Base
rolify
authenticates_with_sorcery!
attr_accessible :username, :email, :password, :password_confirmation
validates_confirmation_of :password
validates_presence_of :password, :on => :create
validates_presence_of :username
validates_uniqueness_of :username
validates_presence_of :email
validates_uniqueness_of :email
end
The Role Model This One:
class Role < ActiveRecord::Base
has_and_belongs_to_many :users, :join_table => :users_roles
belongs_to :resource, :polymorphic => true
end
And From the UsersController we have:
def new
#user = User.new
end
def create
#user = User.new(params[:user])
if #user.save
redirect_to users_path, :notice => "Tu usuario se ha guardado"
else
render "new"
end
end
Finally the Rolify Migration is this one:
class RolifyCreateRoles < ActiveRecord::Migration
def change
create_table(:roles) do |t|
t.string :name
t.references :resource, :polymorphic => true
t.timestamps
end
create_table(:users_roles, :id => false) do |t|
t.references :user
t.references :role
end
add_index(:roles, :name)
add_index(:roles, [ :name, :resource_type, :resource_id ])
add_index(:users_roles, [ :user_id, :role_id ])
end
end
Now, I can assign roles manually from the rails console by using:
1 User.all
2 User.find(id)
3 User.add_role(:role)
But how can I assign automatically a default role when every user it's created?
Thanks!
You can use an active record callback to assign the role after the user is created. Something like
class User < ActiveRecord::Base
after_create :assign_default_role
def assign_default_role
add_role(:role)
end
end
Note that there's also an after_save callback but it's called EVERY time the user is saved. So if you edit the user and save it would try to add the role again. That's why I'm using the after_create callback instead.
You'd better check if a role is assigned before add_role. so I prefer:
class User < ActiveRecord::Base
after_create :assign_default_role
def assign_default_role
add_role(:normal) if self.roles.blank?
end
end
Forget it, Just had to add:
#user.add_role(:user)
in my create action right after the #user = User.new(params[:user]) line.
Figured it out by myself... I'm still learning :)
Thanks!
after_create :default_role
private
def default_role
self.roles << Role.find_by_name("user")
self.save
end

Resources