多页表单的复杂模型验证

发布于 2024-10-27 02:24:47 字数 6541 浏览 0 评论 0原文

我正在尝试使用设备和活跃商家编写注册。该表单很复杂,因为我的用户对象如下所示:

class User < ActiveRecord::Base
  include ActiveMerchant::Utils

  # Include default devise modules. Others available are:
  # :token_authenticatable, :encryptable, :confirmable, :lockable, :timeoutable and :omniauthable
  devise :database_authenticatable, :registerable, 
         :recoverable, :rememberable, :trackable, :validatable, :omniauthable

  # Setup accessible (or protected) attributes
  attr_accessible :email, :password, :password_confirmation, :remember_me, :username, :first_name, 
                  :subscription_attributes, :last_name, :zipcode, 
                  :payment_profile_attributes, :customer_cim_id, :payment_profile_id

...

  # Track multi-page registration
  attr_writer :current_step

...

  # Setup Payment Profile element (authorize.net billing profile)
  has_one :payment_profile, :dependent => :delete
  accepts_nested_attributes_for :payment_profile

现在 PaymentProfile 类有自己的子项,来自活跃商家的两项:

require 'active_merchant'

class PaymentProfile < ActiveRecord::Base
  include ActiveMerchant::Billing
  include ActiveMerchant::Utils

  validate_on_create :validate_card, :validate_address

  attr_accessor :credit_card, :address

  belongs_to :user

  validates_presence_of :address, :credit_card

  def validate_address
    unless address.valid?
      address.errors.each do |error|
        errors.add( :base, error )
      end
    end
  end

  def address
    @address ||= ActiveMerchant::Billing::Address.new(
      :name     => last_name,
      :address1 => address1,
      :city     => city,
      :state    => state,
      :zip      => zipcode,
      :country  => country,
      :phone    => phone
    )
  end

  def validate_card
    unless credit_card.valid?
      credit_card.errors.full_messages.each do |message|
        errors.add( :base, message )
      end
    end
  end

  def credit_card
    @credit_card ||= ActiveMerchant::Billing::CreditCard.new(
      :type               => card_type,
      :number             => card_number,
      :verification_value => verification_code,
      :first_name         => first_name,
      :last_name          => last_name
    )
    @credit_card.month ||= card_expire_on.month unless card_expire_on.nil?
    @credit_card.year  ||= card_expire_on.year unless card_expire_on.nil?
    return @credit_card
  end

现在我已经重写了 Devise 中的 RegistrationsController,以使用 Ryan Bates 的解决方案处理多页表单多页表单截屏 (http://railscasts.com/episodes/217-multistep-forms)。我必须对其进行一些调整才能使其与 Devise 一起使用,但我成功了。现在,由于 Ryan 的多页面表单只是要求不同页面上同一模型的不同字段,因此他可以通过向其 validate 方法添加 :if 块来覆盖其 valid? 方法,如下所示

validates_presence_of :username, :if => lambda { |o| o.current_step == "account" }

:就我而言,我要求从我的父模型(用户)中获取第一个表单上的所有字段,然后从我的两个孙子模型中请求所有字段(用户:PaymentProfile:地址,用户:PaymentProfile:Credit_Card)在第二页上。

我面临的问题是虽然 PaymentProfile.valid?根据 ActiveMerchant 的逻辑返回错误,表单本身不会呈现甚至不会显示这些错误。计费页面的查看代码如下所示:

<h2>Payment Details</h2>

<%= semantic_form_for(resource, :as => resource_name, :url => registration_path(resource_name)) do |f| %>
    <%= devise_error_messages! %>

    <%= f.semantic_fields_for :payment_profile do |p| %>
        <%= p.semantic_fields_for :address do |a| %>
            <%= a.inputs "Billing Information", :id => "billing" do %>
                <%= a.input :type,    :label => "Credit Card", :as => :select, :collection => get_creditcards %>
                <%= a.input :number,     :label => "Card Number", :as => :numeric %>
                <%= a.input :card_expire_on, :as => :date, :discard_day => true, :start_year => Date.today.year, :end_year => (Date.today.year+10), :add_month_numbers => true %>
                <%= a.input :first_name %>      
                <%= a.input :last_name %>
                <%= a.input :verification_code, :label => "CVV Code" %>
            <% end %>
        <% end %>

        <%= f.semantic_fields_for :credit_card do |c| %>
            <%= c.inputs "Billing Address", :id => "address" do %>
                <%= c.input :address1, :label => "Address" %>
                <%= c.input :city %>
                <%= c.input :state,   :as => :select, :collection => Carmen::states %>
                <%= c.input :country, :as => :select, :collection => Carmen::countries, :selected => 'US' %>
                <%= c.input :zipcode, :label => "Postal Code" %>
                <%= c.input :phone,   :as => :phone %>
            <% end %>
        <% end %>
    <% end %>

    <%= f.commit_button :label => "Continue" %>
    <% unless @user.first_step? %>
    <%= f.commit_button :label => "Back", :button_html => { :name => "back_button" } %>
    <% end %>
<% end %>

我在 valid? 后立即在代码中添加了一条 puts error 消息。命令,它显示如下:

{:base=>[["first_name", ["cannot be empty"]], ["last_name", ["cannot be empty"]], ["year", ["expired", "is not a valid year"]], ["type", ["is required", "is invalid"]], ["number", ["is not a valid credit card number"]], ["verification_value", ["is required"]], ["address1", ["is required"]], ["city", ["is required"]], ["state", ["is required"]], ["zip", ["is required", "must be a five digit number"]], ["phone", ["is required", "must be in the format of 333-333-3333"]]]}
{:base=>[["first_name", ["cannot be empty"]], ["last_name", ["cannot be empty"]], ["year", ["expired", "is not a valid year"]], ["type", ["is required", "is invalid"]], ["number", ["is not a valid credit card number"]], ["verification_value", ["is required"]], ["address1", ["is required"]], ["city", ["is required"]], ["state", ["is required"]], ["zip", ["is required", "must be a five digit number"]], ["phone", ["is required", "must be in the format of 333-333-3333"]]]}

现在,此输出的结构与标准错误输出的输出不匹配,标准错误输出是由单层哈希构建的,例如:

{:username=>["can't be blank"]}

因此,在向您展示所有这些之后,我的问题是: a) 如何正确显示错误输出,以便表单实际将其吐出? b) 如何防止parent.valid?还验证孙子。有效吗?当我不在该页面上时?我无法使用 :if =>; lambda...子模型的解决方案,因为他们不知道 current_step 是什么。

对于这么长的帖子,我深表歉意,我只是想包含尽可能多的信息。我已经和这个问题斗争了一个星期了,但我似乎无法克服它。任何建议都会非常有帮助。提前致谢。

I'm trying to write a registration using devise and active merchant. The form is complex in that my user object looks like this:

class User < ActiveRecord::Base
  include ActiveMerchant::Utils

  # Include default devise modules. Others available are:
  # :token_authenticatable, :encryptable, :confirmable, :lockable, :timeoutable and :omniauthable
  devise :database_authenticatable, :registerable, 
         :recoverable, :rememberable, :trackable, :validatable, :omniauthable

  # Setup accessible (or protected) attributes
  attr_accessible :email, :password, :password_confirmation, :remember_me, :username, :first_name, 
                  :subscription_attributes, :last_name, :zipcode, 
                  :payment_profile_attributes, :customer_cim_id, :payment_profile_id

...

  # Track multi-page registration
  attr_writer :current_step

...

  # Setup Payment Profile element (authorize.net billing profile)
  has_one :payment_profile, :dependent => :delete
  accepts_nested_attributes_for :payment_profile

Now the PaymentProfile class has its own children, two items from active merchant:

require 'active_merchant'

class PaymentProfile < ActiveRecord::Base
  include ActiveMerchant::Billing
  include ActiveMerchant::Utils

  validate_on_create :validate_card, :validate_address

  attr_accessor :credit_card, :address

  belongs_to :user

  validates_presence_of :address, :credit_card

  def validate_address
    unless address.valid?
      address.errors.each do |error|
        errors.add( :base, error )
      end
    end
  end

  def address
    @address ||= ActiveMerchant::Billing::Address.new(
      :name     => last_name,
      :address1 => address1,
      :city     => city,
      :state    => state,
      :zip      => zipcode,
      :country  => country,
      :phone    => phone
    )
  end

  def validate_card
    unless credit_card.valid?
      credit_card.errors.full_messages.each do |message|
        errors.add( :base, message )
      end
    end
  end

  def credit_card
    @credit_card ||= ActiveMerchant::Billing::CreditCard.new(
      :type               => card_type,
      :number             => card_number,
      :verification_value => verification_code,
      :first_name         => first_name,
      :last_name          => last_name
    )
    @credit_card.month ||= card_expire_on.month unless card_expire_on.nil?
    @credit_card.year  ||= card_expire_on.year unless card_expire_on.nil?
    return @credit_card
  end

Now I've overrided the RegistrationsController from Devise to handle the multi-page form using the solution from Ryan Bates multi-page form screencast (http://railscasts.com/episodes/217-multistep-forms). I had to tweak it a bit to get it working with Devise, but I was successful. Now because Ryan's multi-page form simply asked for different fields from the same model on different pages, he was able to override his valid? method by adding an :if block to his validate method a la:

validates_presence_of :username, :if => lambda { |o| o.current_step == "account" }

But in my case, I'm asking for all the fields on the first form from my parent model (User), and then asking for the all the fields from my two grandchild models (User:PaymentProfile:Address, User:PaymentProfile:Credit_Card) on teh second page.

The problem I'm facing is that although PaymentProfile.valid? returns errors based on ActiveMerchant's logic, the form itself doesn't render or even display those errors. The view code for the billing page looks like this:

<h2>Payment Details</h2>

<%= semantic_form_for(resource, :as => resource_name, :url => registration_path(resource_name)) do |f| %>
    <%= devise_error_messages! %>

    <%= f.semantic_fields_for :payment_profile do |p| %>
        <%= p.semantic_fields_for :address do |a| %>
            <%= a.inputs "Billing Information", :id => "billing" do %>
                <%= a.input :type,    :label => "Credit Card", :as => :select, :collection => get_creditcards %>
                <%= a.input :number,     :label => "Card Number", :as => :numeric %>
                <%= a.input :card_expire_on, :as => :date, :discard_day => true, :start_year => Date.today.year, :end_year => (Date.today.year+10), :add_month_numbers => true %>
                <%= a.input :first_name %>      
                <%= a.input :last_name %>
                <%= a.input :verification_code, :label => "CVV Code" %>
            <% end %>
        <% end %>

        <%= f.semantic_fields_for :credit_card do |c| %>
            <%= c.inputs "Billing Address", :id => "address" do %>
                <%= c.input :address1, :label => "Address" %>
                <%= c.input :city %>
                <%= c.input :state,   :as => :select, :collection => Carmen::states %>
                <%= c.input :country, :as => :select, :collection => Carmen::countries, :selected => 'US' %>
                <%= c.input :zipcode, :label => "Postal Code" %>
                <%= c.input :phone,   :as => :phone %>
            <% end %>
        <% end %>
    <% end %>

    <%= f.commit_button :label => "Continue" %>
    <% unless @user.first_step? %>
    <%= f.commit_button :label => "Back", :button_html => { :name => "back_button" } %>
    <% end %>
<% end %>

I added a puts errors message in my code right after the valid? command and it shows as follows:

{:base=>[["first_name", ["cannot be empty"]], ["last_name", ["cannot be empty"]], ["year", ["expired", "is not a valid year"]], ["type", ["is required", "is invalid"]], ["number", ["is not a valid credit card number"]], ["verification_value", ["is required"]], ["address1", ["is required"]], ["city", ["is required"]], ["state", ["is required"]], ["zip", ["is required", "must be a five digit number"]], ["phone", ["is required", "must be in the format of 333-333-3333"]]]}
{:base=>[["first_name", ["cannot be empty"]], ["last_name", ["cannot be empty"]], ["year", ["expired", "is not a valid year"]], ["type", ["is required", "is invalid"]], ["number", ["is not a valid credit card number"]], ["verification_value", ["is required"]], ["address1", ["is required"]], ["city", ["is required"]], ["state", ["is required"]], ["zip", ["is required", "must be a five digit number"]], ["phone", ["is required", "must be in the format of 333-333-3333"]]]}

Now the structure of this output doesn't match the output of a standard error output which is built off a single layer hash such as:

{:username=>["can't be blank"]}

So after showing you all of that, my questions are these:
a) how do I get the error output to show properly so that the form actually spits them out?
b) how do I prevent the parent.valid? from also validating the grandchildren.valid? when I'm not on that page? I can't use the :if => lambda... solution on child models because they don't know what the current_step is.

My apologies for such a long post, I just wanted to include as much information as possible. I've been wrestling with this for a week now and I can't seem to get past it. Any advice would be hugely helpful. Thanks in advance.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(3

清风挽心 2024-11-03 02:24:47

错误未显示的原因可能是它们是在基础上填充的,而不是在单个属性上填充的。这发生在您的 validate_cardvalidate_address 方法中。您应该将错误添加到导致错误的特定属性,而不是将错误添加到基础中。

errors.add( attr , error )

其次,如果您想让验证依赖于某个状态,如您提到的截屏视频,那么您需要将状态标志与模型一起保存(可能最好是父级)。您可以手动执行此操作,或者更好的是,您可以使用 gem 来执行此操作(推荐): state_machine

好运气。

The reason the errors aren't showing is probably that they are populated on the base, not on the individual attributes. This happens in your validate_card and validate_address methods. Instead of adding errors to base, you should add them to the specific attribute that caused the error.

errors.add( attr , error )

Secondly, if you want to make your validations dependent on a certain state, as the screencast you mentioned, then you need to save the state flag with the model (probably best the parent). You can do this by hand or, better, you can use a gem for this (recommended): state_machine

Good luck.

怪我闹别瞎闹 2024-11-03 02:24:47

在较高的层次上,您似乎在对象建模中使用继承,并且该模型以多种形式构建,几乎采用“向导”式的方法。我的建议是对您的对象进行建模以反映实际的形式,

First part of the form collect basic User information : UserInformation model 

Second Part of the form collect payment related information: PaymentInformation model (the Active merchant stuff)

等等......

其中用户模型有一个 UserInformation,有一个 PaymentInformation 等等。

本质上用组合代替继承。尝试看看是否也可以避免扩展 ActiveMerchant 框架。

上述样式可以让您更好地控制何时调用#valid?在数据模型的子集上。当用户在表单中移动时,它会被逐部分构建。

抱歉,我没有适合您的具体解决方案,但有更通用的重写方法。

On a high level, you seem to be using inheritance in your object modeling and this model is getting built in several forms, in almost 'wizard' like approach. My suggestion would be to model your objects to reflect, the actual forms like,

First part of the form collect basic User information : UserInformation model 

Second Part of the form collect payment related information: PaymentInformation model (the Active merchant stuff)

and so on...

Where either the User model has one UserInformation, has one PaymentInformation and so on.

Essentially replace inheritance with Composition. Try and see if you can avoid extending the ActiveMerchant frame work too.

The above style, will give you more control over when you want to call #valid? on a subset of you data model. It get constructed part by part as the user move through the form.

Sorry, I dont have specific solution for you but a more general rewrite approach.

春风十里 2024-11-03 02:24:47

我是 Ruby-on-Rails 的新手,我知道这不能回答上述问题,但您应该尝试 Client-侧面验证并查看Rails-casts。也许对你有帮助!

I am new to Ruby-on-Rails and I know this doesn't answer the questions above but you should try out Client-Side Validations and take a look at the Rails-casts. It may be helpful to you!

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文