Question

I'm working on adding an "account plan" select field to a Devise sign-up form.

My user model is called User and my account plan model is called AccountPlan. Since the majority of users are expected to not actually be accountholders, User is connected to AccountPlan via PaidAccount, i.e. User has one AccountPlan through PaidAccount.

class User < ActiveRecord::Base
  has_one :paid_account
  has_one :account_plan, through: :paid_account
end

And then here's the relevant part of my form:

<%= form_for(resource, as: resource_name, url: registration_path(resource_name), html: { id: "payment-form" }) do |f| %>
  <%= f.collection_select :account_plan, @account_plans, :id, :name_with_price %>
<% end %>

My problem is that when I submit the form (which was working perfectly up until I added the :account_plan field), I get

undefined method `id' for "2":String

It tells me that the offending line is super, which is of course not particularly helpful. I figured out that the offending line is this, so it evidently doesn't like something about the params.

The confusing thing, though, is that if I do User.new(account_plan: AccountPlan.new) on the console, it takes that just fine. So if my form is spitting out this HTML

<select id="user_account_plan" name="user[account_plan]"><option value="2">Lite ($10.00/mo)</option>
  <option value="3">Professional ($20.00/mo)</option>
  <option value="4">Plus ($30.00/mo)</option>
</select>

then I don't get what the problem should be.

If it helps, here's my controller code:

class RegistrationsController < Devise::RegistrationsController
  def new
    @account_plans = AccountPlan.order(:price)
    super
  end

  def create
    super
    Stripe.api_key = Rails.configuration.stripe_secret_key
    Stripe::Customer.create(
      card: params[:stripeToken],
      description: resource.email
    )
  end
end
Was it helpful?

Solution

I am guessing the PaidAccount contains a lot of extra attributes for a paid-account, including aspecification of the account-plan, which is a list of available plans. So your relation should look like

class PaidAccount < ActiveRecord::Base
  belongs_to :account_plan
end

(which means that the PaidAccount has a attribute account_plan_id)

There are two approaches: use a nested form, or use something more ad hoc.

Using a nested form

I am using simple_form and haml --> please check into that, makes the code a lot easier to write and show :)

So in that case, I would do the following:

class User 
  has_one :paid_account
  has_one :account_plan, through: :paid_account

  accepts_nested_attributes_for :paid_account
end

Because we are not creating a new account_plan, we are linking to an existing one.

Your form would simply become:

= simple_form_for(resource, as: resource_name, url: registration_path(resource_name), html: { id: "payment-form" }) do |f|
  = f.simple_fields_for :paid_account do |pa|
    = pa.association :account_plan

The problem with this approach: when do you create the paid_account, a bit hard to tell without seeing the rest of your code. I see two options:

  • you only want to create it when an account-plan is chosen.
  • or you allow users only to select an account-plan, when they already have decided to be paying (and you can already create the PaidAccount object).

I am guessing it is the first option. For the nested form to work, you will always have to create the nested PaidAccount and delete it when saving when the account-plan is not filled in. That is actually easy to do, so we add

accepts_nested_attributes_for :paid_account, reject_if: :blank

And your controller new action should become:

  def new
    @account_plans = AccountPlan.order(:price)
    build_resource({})
    resource.build_paid_account 
    respond_with self.resource
  end

Also note you will have to correctly specify your strong parameters, so your permits should look something like

    def post_params
      params.require(:user).permit(:email, :password, :paid_account_attributes => [:account_path_id])
    end

The more ad hoc approach

  • use the view as you had before
  • allow the account_plan in your post_params (as above)
  • check the value of account_plan in the create action, and build a PaidAccount with the correct account plan if needed.

So your create action would look like

def create
  account_plan_id = params[:user].delete(:account_plan)
  super
  if account_plan_id.present?
    resource.build_paid_account(account_plan_id: account_plan_id)
  end
  Stripe.api_key = Rails.configuration.stripe_secret_key
  Stripe::Customer.create(
    card: params[:stripeToken],
    description: resource.email
  )
end

OTHER TIPS

I think you have 2 problems there:

  • in the collection_select use :account_plan_id as the method for your object

  • you have to "sanitize" your params (because of the strong parameters in Rails 4- read more here https://github.com/plataformatec/devise#strong-parameters) for additional fields with devise. So in your registrations_controller add the accepted params like:

    def sign_up_params params.require(:user).permit(:email, :password, :password_confirmation, :current_password, :account_plan_id and whatever else you need in registration ) end

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top