Devise: User + Profile 
Tags: devise, rails, ruby
Published: 2021-08-21 10:31:28

## Why?
Students of the [#pivorak Ruby Summer Courses 2021](https://pivorak.com) 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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/kztzk06lo2ewi00nwdqo.png)

## What we had?
### Database structure
#### Users migration
##### `db/migrations/20210810072523_devise_create_users.rb`
```ruby
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`
```ruby
class User < ApplicationRecord
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :validatable

  has_one :profile
end
```

#### Profiles migration
##### `db/migrations/20210810073644_create_profiles.rb`
```ruby
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`
```ruby
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:
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
```bash
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`
```ruby
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
```bash
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`
```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>
```
![image](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/egqw4amh4poyerj9nt1i.png)

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`
```ruby
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:
![image](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/qk2s40y2qc5dvxbtayf6.png)
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`
```ruby
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`
```ruby
class User < ApplicationRecord
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :validatable

  has_one :profile

  accepts_nested_attributes_for :profile
end
```

## That's all folks!