devise and multiple "user" models - ruby-on-rails

I'm using rails 3.2 and devise 2.0 and I'm quite new to Rails.
Requirements
I'd like to achieve the following:
have 2 or more "user" models, eg. Member, Customer, Admin
all models share some required fields (eg. email and password)
each model may have some unique fields (eg. company for Customer only)
some fields may be shared but not have the same validation (eg. name is required for Customer but optional for Member)
all fields must be filled during the registration process, so the forms are different
the login form should be unique
Possible solutions
I googled and searched StackOverflow for quite a long time, but nothing seems right to me (I'm a Java guy, sorry :) and now I'm quite confused. Two solutions came up:
Single devise user
That's the most frequent answer. Just create the default devise User and create relations between Member-->User and Customer-->User.
My concern here is how can I achieve a customized registration process for each model? I tried different things but all ended as a mess!
Multiple devise users
This solves the custom registration process, and seems right to me, but the unique login form is a blocker. I found an answer on SO (Devise - login from two model) which suggests to override Devise::Models::Authenticatable.find_for_authentication(conditions).
That seems complicated (?) and since I'm new to rails, I'd like to know if that could work?
Thanks for your advice!

Welcome aboard Java guy =), I hope you'll enjoy the Rails world.
Simply, to solve your issue you have 2 solutions:
For each user create a table in the database and corresponding model.
Create a single table in the database and for each user type create a model. This is called single table inheritance (STI).
Which one to choose?
It depends on the common attributes of the roles. If they are almost common (for example all have a name, email, mobile, ...) and a few attributes are different, I highly recommend the STI solution.
How to do the STI?
1. Simply create the the devise user model and table using the command rails generate devise User
2. Add a column named type with string datatype to the user table in the database using a migration.
3. For each user type create a model (for example rails g model admin)
4. Make the Admin class inherits from user model
class Admin < User
end
That's it you are done =) ... Yupeee
To create an admin run the command Admin.create(...) where the dots is the admin attributes for example the email, name, ...
I think this question could help you too

I'm in similar shoes as you, after trying all sorts of approaches I went with a single User model, which would belong to polymorphic roles. This seems like the simplest way to achieve single-login.
The User model would contain the information specific to log-in only.
The Role model would store fields specific to each role, as well as other associations specific to the role.
New registrations would be customized for each user type (roles) via individual controllers, and then building nested attributes for the User.
class User < ActiveRecord::Base
#... devise code ...
belongs_to :role, :polymorphic => true
end
class Member < ActiveRecord::Base
attr_accessible :name, :tel, :city #etc etc....
attr_accessible :user_attributes #this is needed for nested attributes assignment
#model specific associations like
has_many :resumes
has_one :user, :as => :role, dependent: :destroy
accepts_nested_attributes_for :user
end
Routes -- just regular stuff for the Member model.
resources :members
#maybe make a new path for New signups, but for now its new_member_path
Controller -- you have to build_user for nested attributes
#controllers/members_controller.rb
def new
#member = Member.new
#member.build_user
end
def create
#... standard controller stuff
end
views/members/new.html.erb
<h2>Sign up for new members!</h2>
<%= simple_form_for #member do |f| %>
# user fields
<%= f.fields_for :user do |u| %>
<%= u.input :email, :required => true, :autofocus => true %>
<%= u.input :password, :required => true %>
<%= u.input :password_confirmation, :required => true %>
<% end %>
# member fields
<%= f.input :name %>
<%= f.input :tel %>
<%= f.input :city %>
<%= f.button :submit, "Sign up" %>
<% end %>
I would like to point out that there is NO NEED to reach for nested_form gem; since the requirement is that User can only belong_to one type of Role.

I found a way to go and I'm quite happy with it so far. I'll describe it here for others.
I went with the single "user" class. My problem was to achieve a customized registration process for each pseudo model.
model/user.rb:
class User < ActiveRecord::Base
devise :confirmable,
:database_authenticatable,
:lockable,
:recoverable,
:registerable,
:rememberable,
:timeoutable,
:trackable,
:validatable
# Setup accessible (or protected) attributes for your model
attr_accessible :email, :password, :password_confirmation, :remember_me, :role
as_enum :role, [:administrator, :client, :member]
validates_as_enum :role
## Rails 4+ for the above two lines
# enum role: [:administrator, :client, :member]
end
Then I adapted http://railscasts.com/episodes/217-multistep-forms and http://pastie.org/1084054 to have two registration paths with an overridden controller:
config/routes.rb:
get 'users/sign_up' => 'users/registrations#new', :as => 'new_user_registration'
get 'clients/sign_up' => 'users/registrations#new_client', :as => 'new_client_registration'
post 'clients/sign_up' => 'users/registrations#create', :as => 'client_registration'
get 'members/sign_up' => 'users/registrations#new_member', :as => 'new_member_registration'
post 'members/sign_up' => 'users/registrations#create', :as => 'member_registration'
controllers/users/registrations_controller.rb:
I created a wizard class which knows the fields to validate at each step
class Users::RegistrationsController < Devise::RegistrationsController
# GET /resource/sign_up
def new
session[:user] ||= { }
#user = build_resource(session[:user])
#wizard = ClientRegistrationWizard.new(current_step)
respond_with #user
end
# GET /clients/sign_up
def new_client
session[:user] ||= { }
session[:user]['role'] = :client
#user = build_resource(session[:user])
#wizard = ClientRegistrationWizard.new(current_step)
render 'new_client'
end
# GET /members/sign_up
def new_member
# same
end
# POST /clients/sign_up
# POST /members/sign_up
def create
session[:user].deep_merge!(params[:user]) if params[:user]
#user = build_resource(session[:user])
#wizard = ClientRegistrationWizard.new(current_step)
if params[:previous_button]
#wizard.previous
elsif #user.valid?(#wizard)
if #wizard.last_step?
#user.save if #user.valid?
else
#wizard.next
end
end
session[:registration_current_step] = #wizard.current_step
if #user.new_record?
clean_up_passwords #user
render 'new_client'
else
#session[:registration_current_step] = nil
session[:user_params] = nil
if #user.active_for_authentication?
set_flash_message :notice, :signed_up if is_navigational_format?
sign_in(:user, #user)
respond_with #user, :location => after_sign_up_path_for(#user)
else
set_flash_message :notice, :"signed_up_but_#{#user.inactive_message}" if is_navigational_format?
expire_session_data_after_sign_in!
respond_with #user, :location => after_inactive_sign_up_path_for(#user)
end
end
end
private
def current_step
if params[:wizard] && params[:wizard][:current_step]
return params[:wizard][:current_step]
end
return session[:registration_current_step]
end
end
and my views are:
new.rb
new_client.rb including a partial according to the wizard step:
_new_client_1.rb
_new_client_2.rb
new_member.rb including a partial according to the wizard step:
_new_member_1.rb
_new_member_2.rb

So what's wrong? Just run rails g devise:views [model_name], customize each registration forms and in config/initializer/devise.rb just put config.scoped_views = true.

Related

Rails5. Nested attributes are not going to database

class User < ApplicationRecord
has_one :address
accepts_nested_attributes_for :address
end
class Address < ApplicationRecord
belongs_to :user
end
<%= form_for #user do |f| %>
.... // some filed here everything fine
<%= f.fields_for :address do |a| %>
<%= a.text_field :city %> // this field is not appear
<% end %>
<% end %>
class UsersController < ApplicationController
def new
#user = User.new
end
def create
#user = User.new(user_params)
if #user.valid?
#user.save
else
redirect_to root_path
end
end
private
def user_params
params.require(:user).permit(:id, :name, :email, :password, :password_confirmation, :status, :image, :address_attributes => [:id, :city, :street, :home_number, :post_code, :country])
end
end
So like you can see above I have two classes and one form, when I am trying display fields for Address class I can not do it in that way. I took this example from https://apidock.com/rails/ActionView/Helpers/FormHelper/fields_for
I was trying different combination like for example using User.new and Address.new in form definition it not working as well, I was able display all fields in that situation but I wasn't able to save Address data to table, because of "unpermited address".
Can someone explain what I am doing wrong? Or at least give me please some hints.
[SOLVED]
I should learn how to read documentations properly. Excalty like #Srack said I needed just use build_address method. I checked documentation rails api again and on the end of page there was examples says to create User class like this:
class User < ApplicationRecord
has_one :address
accepts_nested_attributes_for :address
def address
super || build_address
end
end
and that solved my issue.
Thank you.
You'll have to make sure there's an address instantiated for the user in the new view. You could do something like:
def new
#user = User.new
#user.build_address
end
You should then see the address fields on the form.
The nested_fields_for show the fields for a record that's been initialised and belong to the parent. I think the latter is why your previous attempts haven't worked.
FYI build_address is an method generated by the belongs_to association: http://guides.rubyonrails.org/association_basics.html#methods-added-by-belongs-to

How to define params with has_many through?

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.

Custom fields in form_for

Trying to create a profile form in which a user can add information about their hobbies to their user profile. But I get the following error when trying to do so.
NoMethodError in Users#edit_profile
undefined method `hobbies' for #<User:0x007ff1a8a1f198>
Does anyone know what the problem could be, or a potential solution? I'm new to rails but I was under the impression that 'text_field' was a safe bet to make any custom input work. Would installing the strong parameters gem help this out at all?
edit_profile.html.erb
<h2>Tell us about yourself</h2>
<%= form_for #user do |f| %>
<%= f.label :first_name %><br />
<%= f.text_field :first_name, autofocus: true %>
<%= f.label :last_name %><br />
<%= f.text_field :last_name %>
<%= f.label :hobbies %><br />
<%= f.text_field :hobbies %>
<div><%= f.submit "Update" %></div>
<% end %>
user_controller.rb
class UsersController < ApplicationController
before_filter :authenticate_user!
def index
#users = User.all
end
def show
#user = User.find(params[:id])
end
def new
#user = User.new
end
def create
end
def edit
end
def update
#user = User.find(params[:id])
#user.update!(user_params)
redirect_to #user
end
def destroy
end
def edit_profile
#user = User.find(params[:id])
end
def user_params
params.require(:user).permit(:first_name, :last_name, :email, :password, :password_confirmation, :hobbies)
end
end
user.rb
class User < ActiveRecord::Base
# Include default devise modules. Others available are:
# :confirmable, :lockable, :timeoutable and :omniauthable
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :trackable, :validatable
attr_accessible :first_name, :last_name, :email, :password, :password_confirmation, :remember_me, :hobbies
#validates :first_name, presence: true, length: { maximum: 50 }
#validates :last_name, presence: true, length: { maximum: 50 }
end
You don't mention it, but I will assume you're running Rails 4.x.
Rails 4.x introduced strong parameters, so in your controller you need to add a private method to set the allowed parameters and remove the attr_accessible from your model.
So in you case it will be:
def user_params
params.require(:first_name, :last_name).permit(:email, :password, :password_confirmation, :remember_me, :hobbies)
end
If you still have trouble to understand the concept, or came from a previous Rails version, take a look at this blog post.
Does anyone know what the problem could be, or a potential solution?
Sure - the problem is you don't have a hobbies attribute in your User model :)
Since you're new, I'll explain a bit about Rails after I answer the question. However, let me explain the value of creating the right attributes:
#app/models/user.rb
class User < ActiveRecord::Base
attr_accessor :hobbies
end
This is what you'll need to create a single attribute for your User model. The attr_accessor directive is a Ruby method which sets a getter and setter in your User model
This will give Rails the ability to populate the hobbies attribute of your Model. However, the data will not be persistent, as it will only be set on a per-instance basis in the model, meaning it will be lost when you refresh the page etc
Using the code above should get your form working, regardless of whether you're using Rails 3 or 4.
Models
Rails is famously an MVC framework - which means it has 3 core components - models (builds data from the database), controllers (configure data for the view) & views (displays the data from the controller & model).
When you load a Rails application, you're sending a request, which will be routes to a particular controller action. This will then call data from your database, allowing you to manipulate it in your view.
Models, therefore have to be populated from your database. They do this by taking the various attributes you have in your datatables, and creating a series of getters and setters for them. These give you the ability to access the data within the attributes, or set new ones.
Your error occurs because you don't have the relevant attribute set in your datatable. You'll need to create a migration to add the hobbies attribute to your User model:
> $ rails g migration AddHobbiesToUser hobbies:string
> $ rake db:migrate
The migration should create something like:
class AddPartNumberToProducts < ActiveRecord::Migration
def change
add_column :users, :hobbies, :string
end
end

Devise - Insert data into HAS_ONE related model during sign up

I have a User model, that has_one Profile.
Profile is the place where all the user stuff is saved (name, phone, address, state, etc).
During sign up I need to let user fill in those fields.
Tried to do nested fields but it doesn't really work and I don't really understand why.
Does anyone have similar code examples? Can't find anything in Internet.
Candidate has_one :profile
Profile belongs_to :user
Registration form:
= simple_form_for(:candidate,
as: Candidate,
url: candidate_registration_path) do |f|
= f.simple_fields_for :profile do |profile|
= profile.input :first_name
= profile.input :last_name
= f.input :email
= f.input :password
= f.input :password_confirmation
= f.submit 'Start Building', class: 'btn btn-primary'
Didn't do anything with controllers except this:
def configure_devise_params
devise_parameter_sanitizer.for(:sign_up) do |u|
u.permit(:email, :password, :password_confirmation,
profile_attributes: [:first_name, :last_name])
end
end
When you say it doesn't work do you mean that it doesn't save? Or it doesn't show the fields?
In the latter case, you would have to build the blank profile in the registrations controller before the action is hit. So basically override the devise controller and do something like this:
class RegistrationsController < Devise::RegistrationsController
def new
user = build_resource({})
user.build_profile if user.profile.blank?
respond_with self.resource
end
end
routes.rb
devise_for :candidates, :controllers => {:registrations => "registrations"}
Candidate.rb
has_one :profile
accepts_nested_attributes_for :profile
and make sure the code you have written above for strong parameters is in your application_controller.
This is also assuming your devise model is called "Candidate"
Check the params in your log. Try creating a user in console using those params. Do you have accepts_nested_attributes_for :profile on your user model?

Associating (has_one) multiple models in form

I'm unable to associate the foreign key before saving the data (the field 'user_id' for the Network model is blank when stored in the database). I'm new to RoR so please excuse me if my code is sloppy :-)
Models:
class Network < ActiveRecord::Base
belongs_to :user
end
class User < ActiveRecord::Base
has_one :network, :foreign_key => "user_id",
:dependent => :destroy
Controller:
class UsersController < ApplicationController
def new
#user = User.new
#network = #user.build_network
#title = "Sign Up"
end
def create
#user = User.new(params[:user])
if (#user.save)
#network = Network.new(params[:network])
if (#network.save)
sign_in #user
flash[:success] = "Welcome!"
redirect_to #user
else
#title = "Sign up"
render 'new'
end
else
#title = "Sign up"
render 'new'
end
end
I'm able to get all of the user input from the view without issues (using form_for and fields_for). Do I have to explicitly define the has_one association in addition to using #user.build_network ?
Per the suggestion of using accepts_nested_attributes_for, I cleaned up my controller to be
def new
#user = User.new
#user.build_network
#title = "Sign Up"
end
def create
#user = User.new(params[:user])
if (#user.save)
sign_in #user
flash[:success] = "Welcome!"
redirect_to #user
else
#title = "Sign up"
render 'new'
end
end
The updated association in the User Model:
has_one :network, :class_name => 'Network',
:foreign_key => "user_id",
:dependent => :destroy
accepts_nested_attributes_for :network,
:reject_if => :all_blank,
:allow_destroy => true
However, the network_attributes are all blank when I submit the form. I have followed the directions in the links provided as well as http://ryandaigle.com/articles/2009/2/1/what-s-new-in-edge-rails-nested-attributes (and many more)
I don't think it's an issue with the view/form since I'm able to access the attributes using Network.new(params[:network]
Any thoughts/suggestions ?
This is a perfect situation for using accepts_nested_attributes_for
Have a look at http://api.rubyonrails.org/classes/ActiveRecord/NestedAttributes/ClassMethods.html as well as http://api.rubyonrails.org/classes/ActionView/Helpers/FormHelper.html#method-i-fields_for to see how you can easily implement it using form_for
(post-question update)
If you can access the attributes for Network through params[:network], then you have (unfortunately!) missed one of the more subtle parts of nested attributes, which is the invocation of fields_for
I'm assuming you have something along the lines of the following (using HAML syntax for speed):
= form_for #user do |f|
= f.text_field :name
= f.text_field :email_address
- fields_for #network do |n|
= n.text_field :name
The problem with this is that Rails isn't seeing any explicit connection in the form between your User and Network fields. (It's smart, but not that smart)
The way you explicitly state that Network is nested within User (for the sake of updating nested attributes is to make sure you call the fields_for #network function on the User form builder, not by itself:
= form_for #user do |f|
= f.text_field :name
= f.text_field :email_address
- f.fields_for #network do |n|
= n.text_field :name
That way, when you submit your form and inspect params, you'll notice that you've got both params[:user] and params[:user][:network] - which Rails will recognise as nested attributtes and should then save them and link them together.
It's possible you need to set network as attr_accessible, along with other attributes of User model, like this:
attr_accessible :name, :age, :network_attributes, ...
Please note you'll also need to put all attributes you want to 'mass assign' in the list, just like :name and :age in the above example.

Resources