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)