多页表单的复杂模型验证
我正在尝试使用设备和活跃商家编写注册。该表单很复杂,因为我的用户对象如下所示:
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 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(3)
错误未显示的原因可能是它们是在基础上填充的,而不是在单个属性上填充的。这发生在您的
validate_card
和validate_address
方法中。您应该将错误添加到导致错误的特定属性,而不是将错误添加到基础中。其次,如果您想让验证依赖于某个状态,如您提到的截屏视频,那么您需要将状态标志与模型一起保存(可能最好是父级)。您可以手动执行此操作,或者更好的是,您可以使用 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
andvalidate_address
methods. Instead of adding errors to base, you should add them to the specific attribute that caused the 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.
在较高的层次上,您似乎在对象建模中使用继承,并且该模型以多种形式构建,几乎采用“向导”式的方法。我的建议是对您的对象进行建模以反映实际的形式,
等等......
其中用户模型有一个 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,
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.
我是 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!