0
votes

I've been grappling with this for a while and tried quite a few options, have watched the http://railscasts.com/episodes/26-hackers-love-mass-assignment?view=asciicast and http://railscasts.com/episodes/196-nested-model-form-part-1 Railscasts and have tried the Devise solution on their Github page but cannot find a solution to this issue.

I'm using Rails 3.2

I have a User which has many addresses.

User Model:

class User < ActiveRecord::Base
  rolify
  # Include default devise modules. Others available are:
  # :token_authenticatable, :confirmable,
  # :lockable, :timeoutable and :omniauthable
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :trackable, :validatable

  # Setup accessible (or protected) attributes for your model
  attr_accessible :name, :email, :password, :password_confirmation, :remember_me, :stripe_token, :coupon, :addresses_attributes
  attr_accessor :stripe_token, :coupon
  before_save :update_stripe
  before_destroy :cancel_subscription

  has_many :addresses
  accepts_nested_attributes_for :addresses, :reject_if => :all_blank, :allow_destroy => true

  def update_plan(role)
    self.role_ids = []
    self.add_role(role.name)
    unless customer_id.nil?
      customer = Stripe::Customer.retrieve(customer_id)
      customer.update_subscription(:plan => role.name)
    end
    true
  rescue Stripe::StripeError => e
    logger.error "Stripe Error: " + e.message
    errors.add :base, "Unable to update your subscription. #{e.message}."
    false
  end

  def update_stripe
    return if email.include?(ENV['ADMIN_EMAIL'])
    return if email.include?('@example.com') and not Rails.env.production?
    if customer_id.nil?
      if !stripe_token.present?
        raise "Stripe token not present. Can't create account."
      end
      if coupon.blank?
        customer = Stripe::Customer.create(
          :email => email,
          :description => name,
          :card => stripe_token,
          :plan => roles.first.name
        )
      else
        customer = Stripe::Customer.create(
          :email => email,
          :description => name,
          :card => stripe_token,
          :plan => roles.first.name,
          :coupon => coupon
        )
      end
    else
      customer = Stripe::Customer.retrieve(customer_id)
      if stripe_token.present?
        customer.card = stripe_token
      end
      customer.email = email
      customer.description = name
      customer.save
    end
    self.last_4_digits = customer.cards.data.first["last4"]
    self.customer_id = customer.id
    self.stripe_token = nil
  rescue Stripe::StripeError => e
    logger.error "Stripe Error: " + e.message
    errors.add :base, "#{e.message}."
    self.stripe_token = nil
    false
  end

  def cancel_subscription
    unless customer_id.nil?
      customer = Stripe::Customer.retrieve(customer_id)
      unless customer.nil? or customer.respond_to?('deleted')
        if customer.subscription.status == 'active'
          customer.cancel_subscription
        end
      end
    end
  rescue Stripe::StripeError => e
    logger.error "Stripe Error: " + e.message
    errors.add :base, "Unable to cancel your subscription. #{e.message}."
    false
  end

  def expire
    UserMailer.expire_email(self).deliver
    destroy
  end

end

User controller:

  def new

    @plan = params[:plan]
    if @plan && ENV["ROLES"].include?(@plan) && @plan != "admin"

      super

    @user.addresses.build

    else
      redirect_to root_path, :notice => 'Please select a subscription plan below.'
    end
  end

Address Model;

class Address < ActiveRecord::Base
  attr_accessible :country, :county, :first_line, :postcode, :second_line, :town_city
  #validates :country, :user_id, :county, :first_line, :postcode, :town_city, presence: true
  belongs_to :user
end

Addresses controller:

class AddressesController < ApplicationController
def create
end

def update
end
end

In my view I am nesting the address model so I can create a user and an address in one foul swoop:

<div class="intro2">

  <% content_for :head do %>
  <%= tag :meta, :name => "stripe-key", :content => STRIPE_PUBLIC_KEY %>
<% end %>

<div id="stripe_error" class="alert alert-error" style="display:none" >
</div>
<%= simple_form_for(resource, :as => resource_name, :url => registration_path(resource_name), :html => {:class => 'card_form form-vertical form-signin' }) do |f| %>
<%= link_to '<i class="icon-step-backward"></i>''back'.html_safe, root_path %>

<h2>Sign up</h2>
  <h3><%= params[:plan].titleize if params[:plan] %> Subscription Plan</h3>
  <%= hidden_field_tag 'plan', params[:plan] %>
  <%= f.error_notification %>
  <%= display_base_errors resource %>
  <%= f.input :name, :autofocus => true %> 
  <%= f.input :email, :required => true %>
  <%= f.input :password, :required => true %>
  <%= f.input :password_confirmation, :required => true %>



<%= field_set_tag 'Customer Details' do %>
            <%= f.fields_for :address do |builder| %>
                <%= builder.label :first_line %>
                <%= builder.text_field :first_line %>
                <%= builder.label :second_line %>
                <%= builder.text_field :second_line %>
                <%= builder.label :town_city %>
                <%= builder.text_field :town_city %>
                <%= builder.label :county %>
                <%= builder.text_field :county %>
                <%= builder.label :postcode %>
                <%= builder.text_field :postcode %>
                <%= builder.label :country %>
                <%= builder.text_field :country %>
                 <% end %>
        <% end %>

  <% if @user.stripe_token %>
    <p>Credit card acceptance is pending.</p>
  <% else %>
    <div class="field">
      <%= label_tag :card_number, "Credit Card Number" %>
      <%= text_field_tag :card_number, nil, name: nil %>
    </div>
    <div class="field">
      <%= label_tag :card_code, "Card Security Code (CVV)" %>
      <%= text_field_tag :card_code, nil, name: nil %>
    </div>
    <div class="field">
      <%= label_tag :card_month, "Card Expiration" %>
      <%= select_month nil, {add_month_numbers: true}, {name: nil, id: "card_month"}%>
      <%= select_year nil, {start_year: Date.today.year, end_year: Date.today.year+10}, {name: nil, id: "card_year"}%>
    </div>
    <div class="field">
      <%= f.input :coupon, :label => 'Promotional Coupon (if any)' %>
    </div>
  <% end %>
  <%= f.hidden_field :stripe_token %>
  <%= f.button :submit, 'Sign up', :class => 'btn-primary' %>
<% end %>
  </div>

The error message I get in the browser is:

ActiveModel::MassAssignmentSecurity::Error at /users Can't mass-assign protected attributes: address

The request parameters are:

{"utf8"=>"✓", "authenticity_token"=>"+69wR2xYkBARu+JlsnizyF+0hrRUXo6Z+XGAipw98NY=", "plan"=>"silver", "user"=>{"name"=>"Bob", "email"=>"[email protected]", "password"=>"12345678", "password_confirmation"=>"12345678", "address"=>{"first_line"=>"7a Annette Road", "second_line"=>"Bob's Landing", "town_city"=>"London", "county"=>"Greater London", "postcode"=>"N76ET", "country"=>"UK"}, "coupon"=>"", "stripe_token"=>"tok_103NDO2KdYRHhGilKFAqsYaA"}, "action"=>"create", "controller"=>"registrations"}

As recommended on the Devise Github page I have tried putting the following in my application controller:

  before_filter :configure_permitted_parameters, if: :devise_controller?

and

def configure_permitted_parameters
  devise_parameter_sanitizer.for(:sign_in) { |u| u.permit(:addresses_attributes) }
end

To no avail. (I also tried permitting 'address' and 'addresses')/

Any solutions really appreciated. (Incidentally, if you notice other code which is not optimal it would be great to know this too - keen to pick up good habits).

Thanks

2
try <%= f.fields_for :addresses do |builder| %> instead of <%= f.fields_for :address do |builder| %> ? - scaryguy
@scaryguy Doing that results in the address fields not being displayed in the view - jjrgjbdmzkve
in your controller, you're also supposed to add something like @user.addresses.build - scaryguy
@scaryguy Thanks - I have that in there. I've edited my question to include the controllers. - jjrgjbdmzkve

2 Answers

1
votes

For your accepts_nested_attributes_for to work you need to create the form that then creates the following params hash

{:user=> { :addresses_attributes => [ 0 => {:street => "value" ...}, 1=> {:street=>"value2"}]

in order to do that you need to use the

<%= fields_for :addresses do |form| %>

and in the UserController#new action you need to build at least one new address to populate the form. If you don't build a new address, the form_for section will, as you say, not be displayed, because to the form, there are no addresses belonging to the user

def new
  @user = User.new
  @user.addresses.build
  super
end

I think you need to build the addresses association before you call super

0
votes

Solved.

I changed:

 def new
    @plan = params[:plan]
    if @plan && ENV["ROLES"].include?(@plan) && @plan != "admin"
    super
    else
      redirect_to root_path, :notice => 'Please select a subscription plan below.'
    end
  end

To:

  def new
    @plan = params[:plan]
    if @plan && ENV["ROLES"].include?(@plan) && @plan != "admin"

    build_resource({})
    @user.addresses.build
    respond_with self.resource

    else
      redirect_to root_path, :notice => 'Please select a subscription plan below.'
    end
  end

Turns out

    @user.addresses.build

Needed to be between two lines in the Devise new Registration controller.

I'll need to further optimise this, by overriding the Registration controller in the appropriate way.