I have an almost working sign up form with Devise. Whilst it seems to work, it's not saving the fields in my Custaddress
table other than the user_id
. Any help to work out how to have it save the rest of the information would be great. The following is greatly truncated!!
User.rb contains:
class User < ApplicationRecord
has_one :custaddress
accepts_nested_attributes_for :custaddress
end
Custaddress contains:
class Custaddress < ApplicationRecord
has_many :orders
belongs_to :user
end
Registrations controller contains:
As you can see, this just builds on the "standard" Devise controller. There is no create or new here as I assume it's using the standard methods.
class Users::RegistrationsController < Devise::RegistrationsController
invisible_captcha only: :create
protected
def build_resource(hash = {})
self.resource = resource_class.new_with_session(hash, session)
resource.build_custaddress
# Jumpstart: Skip email confirmation on registration.
# Require confirmation when user changes their email only
resource.skip_confirmation!
# Registering to accept an invitation should display the invitation on sign up
if params[:invite] && (invite = AccountInvitation.find_by(token: params[:invite]))
@account_invitation = invite
# Build and display account fields in registration form if enabled
elsif Jumpstart.config.register_with_account?
account = resource.owned_accounts.first
account ||= resource.owned_accounts.new
account.account_users.new(user: resource, admin: true)
end
end
def update_resource(resource, params)
# Jumpstart: Allow user to edit their profile without password
resource.update_without_password(params)
end
def sign_up(resource_name, resource)
if cookies[:ordernum]
order = Order.where(ordernum: cookies[:ordernum]).first
if order
order.update!(user: resource, custaddress: resource.custaddress)
cookies.delete "ordernum"
end
end
sign_in(resource_name, resource)
# If user registered through an invitation, automatically accept it after signing in
if params[:invite] && (account_invitation = AccountInvitation.find_by(token: params[:invite]))
account_invitation.accept!(current_user)
# Clear redirect to account invitation since it's already been accepted
stored_location_for(:user)
end
end
end
My new.html.erb contains:
<%= form_with(model: resource, as: resource_name, url: registration_path(resource_name, invite: params[:invite])) do |f| %>
<%= f.fields_for :custaddress do |cust| %>
<div class="form-group">
<%= cust.label "Apartment/Unit Number", class: "font-bold" %>
<%= cust.text_field :apartment, class: "form-control", placeholder: "Unit 2 or Apartment 307" %>
</div>
And much more!
My application controller has:
class ApplicationController < ActionController::Base
protect_from_forgery with: :exception
include SetCurrentRequestDetails
include SetLocale
include Jumpstart::Controller
include Accounts::SubscriptionStatus
include Users::NavbarNotifications
include Users::TimeZone
include Pagy::Backend
include CurrentHelper
include Sortable
before_action :configure_permitted_parameters, if: :devise_controller?
before_action :masquerade_user!
before_action :store_user_location!, if: :storable_location?
protected
# To add extra fields to Devise registration, add the attribute names to `extra_keys`
def configure_permitted_parameters
extra_keys = [:avatar, :first_name, :last_name, :time_zone, :preferred_language]
signup_keys = extra_keys + [:terms_of_service, :invite, owned_accounts_attributes: [:name], custaddress_attributes: [:address, :apartment, :city, :state, :country, :postcode, :mobile]]
devise_parameter_sanitizer.permit(:sign_up, keys: signup_keys)
devise_parameter_sanitizer.permit(:account_update, keys: extra_keys)
devise_parameter_sanitizer.permit(:accept_invitation, keys: extra_keys)
end
def after_sign_in_path_for(resource_or_scope)
stored_location_for(resource_or_scope) || super
end
# Helper method for verifying authentication in a before_action, but redirecting to sign up instead of login
def authenticate_user_with_sign_up!
unless user_signed_in?
store_location_for(:user, request.fullpath)
redirect_to new_user_registration_path, alert: t("create_an_account_first")
end
end
def require_current_account_admin
unless current_account_admin?
redirect_to root_path, alert: t("must_be_an_admin")
end
end
private
def storable_location?
request.get? && is_navigational_format? && !devise_controller? && !request.xhr?
end
def store_user_location!
# :user is the scope we are authenticating
store_location_for(:user, request.fullpath)
end
end
My logs are showing:
Processing by Users::RegistrationsController#create as JS
17:08:57 web.1 | Parameters: {"authenticity_token"=>"uVphxW4gCQntvHFxRb33dl9cqxv9vlL69Wc2zOMoF1M+pUk8c2HnHwgQFIkMbfmxYraVI7rYBVCPgfSD1u7OHg==", "user"=>{"first_name"=>"[FILTERED]", "last_name"=>"[FILTERED]", "email"=>"[FILTERED]", "password"=>"[FILTERED]", "time_zone"=>"Sydney", "custaddress_attributes"=>{"apartment"=>"", "address"=>"XXXXXXXX", "city"=>"XXXXXXX", "state"=>"XXXXX", "postcode"=>"XXXX", "mobile"=>"XXXXXXXX", "country"=>"XXXXXXX"}, "terms_of_service"=>"1"}, "enc-rmjxhdab"=>"", "button"=>""}
So, I know it's getting the information from the form. However I have no idea where it's saving the Custaddress
record. It only seems to be associating the user_id
:
Custaddress Create (0.2ms) INSERT INTO "custaddresses" ("user_id", "created_at", "updated_at") VALUES ($1, $2, $3) RETURNING "id" [["user_id", 3], ["created_at", "2020-09-25 02:20:39.109187"], ["updated_at", "2020-09-25 02:20:39.109187"]]
12:20:39 web.1 | (33.9ms) COMMIT
Can anyone please help me work out why the custaddress isn't saving. I've spent hours on this and read every article on google (well it certainly feels like it).
I've tried to track this down, to no avail.
Adding an answer for completeness.
Devise's create
action calls the build_resurce
method that you are overriding. the problem is that you overrid it for the new
action but didn't take into account the create
action.
So, you are doing this:
def build_resource(hash = {})
self.resource = resource_class.new_with_session(hash, session)
resource.build_custaddress
For the new
action there's no issue, resource.build_custaddress
will instantiate a new custaddress
object for you to be able to use the fields_for
helper.
The problem is that, for the create
action, at that point resource
already has a custaddress
with the values from the request (set by the previous line), then you do resource.build_custaddress
and replace the current custaddress
with a new empty one.
The solution is to only build a custaddress if it's not nil:
def build_resource(hash = {})
self.resource = resource_class.new_with_session(hash, session)
resource.build_custaddress if resource.custaddress.nil?
That way you'll have a new empty custaddress for the new
action, but respect the values from the request for the create
, edit
or update
actions.