Recently I was working on a Rails app where every user who signed up also needed to be assigned to an account. Since User
and Account
are two different models, this can be a bit ugly to handle when Devise expects a single resource. However, by leveraging Reform we can use the Form Object Pattern (#3) to create a RegistrationForm
that composes Account
and User
for registration but doesn’t require creating unnecessary linkages between the actual models.
In this post, I’ll walk through the code on how I made it work. I won’t delve too deeply on actually getting Devise setup since the Devise documentation does a pretty good job of covering that. Just note that you will need to copy the Devise views into your app and you will need a local RegistrationsController
that inherits from Devise::RegistrationsController
.
The Tests
Let’s start first with the tests. This will show you what we want the final experience to look like.
Controller
This is a basic controller test that lets us validate that we can send a post request with the form parameters and the expected objects get created. Please note, even though we will be using a RegistrationForm
object, Devise thinks that we will be using a User
object and so we need to use :user
params to keep Devise happy.
Please note, I am using factory_girl_rails to get the attributes_for
methods. Also note the necessity to specify the devise.mapping
; this is discussed further down in Some Final Caveats.
Feature
This is a basic feature test to verify that the process works as expected for a regular site user. The page after a successful login contains the word “Welcome”.
The Form Object
As stated above, we are using Reform to implement a Form Object so that we don’t need to insert any logic into the User
or Account
models to support capturing fields for both models on user signup.
First take a look at the code for the entire form, then we’ll walk through it step-by-step.
Standard Reform Stuff
The first half of the code is standard Reform stuff. We are using Compositions for cleaner fields, setting up our properties, and configuring validation.
Satisfying Devise Processes
The #active_for_authentication?
and #authenticatable_salt
methods are to make Devise happy because it’s trying to treat the RegistrationForm
as if it was the User
model with all of the Devise authenticatable
methods mixed in.
Saving the Record
The save
method is the most interesting thing here (combined with #build_resource
from the RegistrationsController
below). The Devise::RegistrationsController#create
method expects to work with a standard ActiveModel object where the model is validated at the same time that it is saved. However, Reform uses the validation process to actually populate data into the object and validate it. Saving (or syncing) is a separate step. Here is a snippet of the Devise #create
method to see what Devise is doing
As you can see, Devise expects the output from resource.save
to indicate if the record is valid. Since we need to check validity differently with Reform, we have our own #save
method in the form which fails fast if the RegistrationForm
is not valid, but actually handles the save if it is.
The Controller
This is a fairly straightforward implantation of a custom Devise controller with one twist.
As noted above, Reform uses the validation process to populate the form with data from the params hash. Since the #build_resource
method is the only time we can get access to the params hash from Devise, we need to validate there so that we have the params data for later usage. But, #build_resource
gets called from both #create
and #new
; and when it’s called from #new
the hash is empty. If we try to validate with an empty hash we’ll show the signup form form with validation errors – definitely not something that is conducive to people signing up.
The View
The view is pretty standard. Only significant thing to note is that we are treating the :company_name
field as if it belongs to the resource
– which in this case it does. It’s not a field on User
, but it is a field on RegistrationForm
which is the resource
for Devise.
Some Final Caveats
Reform and SimpleForm
The latest version of Reform has some issues with SimpleForm. I’ve been working with the Reform maintainer on a patch but you may need to use my fork of Reform.
RSpec and Devise
In order to get RSpec to work correctly with Devise in controller tests, you will need to edit your spec file and your rails_helper.rb
(or spec_helper.rb
if you’re running an older RSpec). The Devise How To: Test controllers with Rails 3 and 4 (and RSpec) wiki page has the details.
March 26, 2015 at 10:05 am
I had to surcharge sign_up in order to be signed in at the end of the registration:
def sign_up(resource_name, resource)
sign_in(resource_name, resource.model[:user])
end
April 3, 2015 at 8:16 am
Thanks, Sylvain! Yes, since the
sign_in
method expects an actualUser
you do need to create your ownsign_up
method in yourRegistrationsController
.October 27, 2015 at 8:02 pm
First off, thanks for this article! I was happy to find it w/ my first google search.
I’m confused where the ‘devise_registrations_controller_snippet.rb’ code is supposed to go? It looks like you’re saying that is the `create` action to be used in the ‘RegistrationsController’, but then right under that is the ‘RegistrationsController’ with a different `create` action.
I have this almost working (w/o using the ‘devise_registrations_controller_snippet.rb’), but when I sign-up a user, it’s creating the User model and then creating *two* Profile models (which is what your Account model is in your example.) The first profile model saves the first & last name params from the form, but the second one only saves the user_id foreign key. It also sends two user ‘welcome_email’ messages.
Any idea what I might be missing here?
October 28, 2015 at 9:15 am
Hi Jordan, I’m glad this article was able to help.
The `devise_registrations_controller_snippet.rb` is actual code from Devise that I included as a reference. You don’t need to add the code into your app because it is already in place from Devise – it should work w/out adding that bit to your code).
Regarding having two Profile models, are you using Reform and a RegistrationForm object? If you are setting up the `RegistrationsController#build_resource` method correctly then Devise should never see your individual User and Profile models and you should be controlling the saving in `RegistrationForm#save`
October 28, 2015 at 5:49 pm
Ah, okay…I was thinking that might be part of the core code in Devise, but then I began to second guess myself.
The ‘two Profile models’ piece I was able to resolve. It was a some additional in my User model code that I overlooked, which was a after_create callback method that was building the Profile model again.
This article was perfect and EXACTLY what I was looking for. Your above implementation is working beautifully for me. Thanks again!!
October 28, 2015 at 6:03 pm
You’re welcome! I’m glad you got it to work.
I’ve started exploring Trailblazer Operations to avoid those sorts of callback issues. You might want to take a look at that.