Search code examples
ruby-on-railsdevise

Attributes not saving with devise and accepts_nested_attributes_for


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.


Solution

  • 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.