Why?
Students of the #pivorak Ruby Summer Courses 2021 have been working on their practical part project "HoldMyDog" (a dog sitting service) and there was a registration form for users. Since we have split the information about user between User
and Profile
models we need to configure devise to save data from one form to both of them.
What we had?
Database structure
Users migration
db/migrations/20210810072523_devise_create_users.rb
class DeviseCreateUsers < ActiveRecord::Migration[6.1]
def change
create_table :users do |t|
## Database authenticatable
t.string :email, null: false, default: ""
t.string :encrypted_password, null: false, default: ""
## Recoverable
t.string :reset_password_token
t.datetime :reset_password_sent_at
## Rememberable
t.datetime :remember_created_at
t.string :role
t.timestamps null: false
end
add_index :users, :email, unique: true
add_index :users, :reset_password_token, unique: true
end
end
User model
app/models/user.rb
class User < ApplicationRecord
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :validatable
has_one :profile
end
Profiles migration
db/migrations/20210810073644_create_profiles.rb
class CreateProfiles < ActiveRecord::Migration[6.1]
def change
create_table :profiles do |t|
t.string :first_name
t.string :last_name
t.string :phone
t.text :description
t.references :user, null: false, foreign_key: true
t.timestamps
end
end
end
Profile model
app/models/profile.rb
class Profile < ApplicationRecord
belongs_to :user
validates :first_name, presence: true
validates :last_name, presence: true
validates :phone, presence: true
validates :description, length: { maximum: 300 }
end
What to do?
First we need to generate devise views and controllers for registration and then modify them accordingly to allow form params for profile to pass.
Devise generators
We can use devise generators:
-
rails generate devise:views
- to generate all devise views -
rails generate devise:controllers
- to generate all devise controllers
1. Generating devise views for registration
bundle exec rails g devise:controllers users -c registrations
This will generate only registrations controller for us.
2. Editing routes.rb
to use our customised controller
config/routes.rb
Rails.application.routes.draw do
devise_for :users, controllers: {
registrations: 'users/registrations'
}
end
This part is important because without explicit routing you will end up using default devise controller.
3. Generating views
bundle exec rails g devise:views users
This will generate all devise views in scope of users.
4. Editing form view
app/views/users/registrations/new.html.erb
<div class="container">
<h2 class="form-header">Sign up now!</h2>
<%= form_for(resource, as: resource_name, url: registration_path(resource_name)) do |f| %>
<%= render "users/shared/error_messages", resource: resource %>
<%= f.fields_for :profile do |pf| %>
<div>
<%= pf.text_field :first_name, placeholder: 'First name *' %>
</div>
<div>
<%= pf.text_field :last_name, placeholder: 'Last name *' %>
</div>
<div>
<%= pf.text_field :phone, placeholder: 'Phone' %>
</div>
<% end %>
<div>
<%= f.email_field :email, autocomplete: "email", placeholder: 'Email *' %>
</div>
<div>
<%= f.password_field :password, autocomplete: "new-password", placeholder: 'Password *' %>
</div>
<div>
<%= f.password_field :password_confirmation, autocomplete: "new-password", placeholder: 'Repeat password *' %>
</div>
<%= f.fields_for :profile do |profile_form| %>
<%= profile_form.text_area :description, cols: 40, rows: 3, placeholder: 'Tell us about yourself ;)' %>
<% end %>
<div>* How would you like to use the service?</div>
<div>
<%= f.radio_button :role, 'sitter', css: 'form-check-input' %>
<%= label :role_sitter, 'I want to hold someone`s pet', css: 'form-check-label' %>
</div>
<div>
<%= f.radio_button :role, 'owner', css: 'form-check-input' %>
<%= label :role_owner, 'I want to give my pet to sitter', css: 'form-check-label' %>
</div>
<%= f.submit "Sign up", class: "btn btn-light sign-up-btn mt-4" %>
<% end %>
</div>
You may notice that the form looks different and we don't have all the fields that we described in our view, that's because we need to modify the new
action for registration and build profile object for form before rendering.
5. Editing new
action: building profile
app/controllers/users/registrations_controller.rb
class Users::RegistrationsController < Devise::RegistrationsController
def new
build_resource({})
resource.build_profile
respond_with resource
end
end
After we built a profile in new action, after reload form will look like this:
It may seem that we're done, but we need to save the data from the form to the database.
6. Permitting profile saving
app/controllers/users/registrations_controller.rb
class Users::RegistrationsController < Devise::RegistrationsController
before_action :configure_sign_up_params, only: [:create]
def new
build_resource({})
resource.build_profile
respond_with resource
end
protected
def sign_up_params
devise_parameter_sanitizer.sanitize(:sign_up) { |user| user.permit(permitted_attributes) }
end
def configure_sign_up_params
devise_parameter_sanitizer.permit(:sign_up, keys: permitted_attributes)
end
def permitted_attributes
[
:email,
:password,
:password_confirmation,
:remember_me,
:role,
profile_attributes: %i[first_name last_name phone description]
]
end
end
This way we allow a user to pass params from form to database, but there is one more step we need to do - since we are passing all params together and we haven't modified the create
action for Users::RegistrationsController
we need to allow User
model to accept attributes for Profile
.
app/models/user.rb
class User < ApplicationRecord
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :validatable
has_one :profile
accepts_nested_attributes_for :profile
end
Top comments (0)