1

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
sissy
  • 2,908
  • 2
  • 28
  • 54
Jason Swett
  • 43,526
  • 67
  • 220
  • 351

2 Answers2

1

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

sissy
  • 2,908
  • 2
  • 28
  • 54
  • The `user` has no `account_plan_id` attribute. OP uses the `has_one` relation, so `paid_account` has the `user_id` and I assume also the `account_plan_id` field. – nathanvda Feb 19 '14 at 15:50
  • @sissy, I think you're right about the sanitization. I am actually doing that already, I just neglected to mention it in my question. – Jason Swett Feb 19 '14 at 17:40
  • I asked a different question which you might know the answer to: http://stackoverflow.com/questions/21888960/confused-about-strong-parameters-with-devise – Jason Swett Feb 19 '14 at 18:16
  • No problem! Your answer was still helpful. – Jason Swett Feb 20 '14 at 03:00
1

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
Jason Swett
  • 43,526
  • 67
  • 220
  • 351
nathanvda
  • 49,707
  • 13
  • 117
  • 139
  • Thanks for the thorough answer. You read my mind in several places! I took your suggestion to use simple_form, but the `<%= pa.association :account_plan %>` part is not outputting any HTML nor showing any errors. Any ideas there? – Jason Swett Feb 19 '14 at 17:38
  • Nevermind; I figured that part out. The base Devise `RegistrationsController` was doing a `respond_with self.resource` and so anything I put after `super` was not running. I copied the two lines of code from the original `RegistrationsController`, put them in mine, added `self.resource.build_paid_account` before the `respond_with`, and my select field started showing up. – Jason Swett Feb 19 '14 at 17:52
  • And the form now submits without errors but my user is not actually getting an `account_plan` record. – Jason Swett Feb 19 '14 at 17:53
  • Ah, and I think it's because I'm probably doing the sanitization wrong. – Jason Swett Feb 19 '14 at 17:53
  • Okay, I'm still stuck but I accepted your answer because it got me past the biggest problems I was having. At this point it's probably best for me to start a separate question for the issue I'm having now. – Jason Swett Feb 19 '14 at 18:04
  • And if you're interested, here's the other question: http://stackoverflow.com/questions/21888960/confused-about-strong-parameters-with-devise – Jason Swett Feb 19 '14 at 18:16
  • Ok, I see it is answered. Great you got it working. How did you fix the missing `_attributes`? I would assume in the `fields_for` you need to give the relation-name. – nathanvda Feb 19 '14 at 22:14
  • I did it like this, but as you can see I'm not totally satisfied with the solution. http://stackoverflow.com/questions/21891120/is-there-a-better-way-to-name-this-field-appropriately – Jason Swett Feb 20 '14 at 03:00