DEV Community

Andrew
Andrew

Posted on

Devise: User + Profile

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.

image

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
Enter fullscreen mode Exit fullscreen mode

User model

app/models/user.rb
class User < ApplicationRecord
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :validatable

  has_one :profile
end
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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:

  1. rails generate devise:views - to generate all devise views
  2. rails generate devise:controllers - to generate all devise controllers

1. Generating devise views for registration

bundle exec rails g devise:controllers users -c registrations
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

image

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
Enter fullscreen mode Exit fullscreen mode

After we built a profile in new action, after reload form will look like this:
image
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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

That's all folks!

Discussion (0)