`form_for` bypassing model accessories. How to make him stop? (Or: how to create your own attribute serializer?)

I installed these methods to automatically encrypt values.

class User < ApplicationRecord def name=(val) super val.encrypt end def name (super() || '').decrypt end 

When I try to submit a form and an error appears (missing phone), the name attribute is displayed distorted.

 <input class="form-control" type="text" value="Mg8IS1LB2A1efAeZJxIDJMSroKcq6WueyY4ZiUX+hfI=" name="user[name]" id="user_name"> 

It works when checks are successful. It also works in the console when I go in turns through my #update controller.

 irb(main):015:0> u = User.find 1 irb(main):016:0> u.name => "Sue D. Nym" irb(main):017:0> u.phone => "212-555-1234" irb(main):018:0> u.update name: 'Sue D. Nym', phone: '' (10.0ms) BEGIN (1.0ms) ROLLBACK => false irb(main):020:0> u.save => false irb(main):029:0> u.errors.full_messages.join ',' => "Phone can't be blank" irb(main):031:0> u.build_image unless u.image => nil irb(main):033:0> u.name => "Sue D. Nym" 
users_controller.rb
  def update @user = User.find current_user.id @user.update user_params if @user.save flash.notice = "Profile Saved" redirect_to :dashboard else flash.now.alert = @user.errors.full_messages.join ', ' @user.build_image unless @user.image render :edit end end 

The view somehow gets the encrypted value without passing #name and only after the verification fails.


I reduced the controller to an absolute minimum, and it worked right after #update . However, it works on the console!

  def update @user = User.find current_user.id @user.update user_params render :edit return 

I reduced my gaze to an absolute minimum and it shows the name, but only outside of form_for . I don’t know yet why.

edit.haml
 =@user.name =form_for @user, html: { multipart: true } do |f| =f.text_field :name 
HTML source
 <span>Sue D. Nym</span> <form class="edit_user" id="edit_user_1" enctype="multipart/form-data" action="/users/1" accept-charset="UTF-8" method="post"> <input name="utf8" type="hidden" value="βœ“"><input type="hidden" name="_method" value="patch"><input type="hidden" name="authenticity_token" value="C/ScTxfENNxCKgzG0qAlPElOKI7nOYxZimQ7BsB64wIWQ9El4+vOAfxX3qHL08rbr0sxRiJnzQti13e4DAgkfQ=="> <input type="text" value="sER9cjwa6Ov5weXjEQN2KJYoTOXtVBytpX/cI/aPrFs=" name="user[name]" id="user_name"> </form> 

I noticed that attributes still returned the encrypted values, so I tried to add this, but form_for still manages to get the encrypted value and put it on the form!

  def attributes attr_hash = super() attr_hash["name"] = name attr_hash end 

Rails 5.0.2

+7
ruby-on-rails activerecord ruby-on-rails-5
source share
3 answers

While you can get around this by overloading name_before_type_case , I think this is actually the wrong place for such a conversion.

According to your example, the requirements here are as follows:

  • plain text in mind
  • encrypted alone

So, if we move the encryption / decryption conversion to the Ruby-DB border, this logic will become much cleaner and more reusable.

Rails 5 introduced a useful attribute API to solve this exact scenario. Since you have not provided any details about how your encryption procedure is implemented, I am going to use Base64 in my code example to demonstrate text conversion.

app/types/encrypted_type.rb
 class EncryptedType < ActiveRecord::Type::Text # this is called when saving to the DB def serialize(value) Base64.encode64(value) unless value.nil? end # called when loading from DB def deserialize(value) Base64.decode64(value) unless value.nil? end # add this if the field is not idempotent def changed_in_place?(raw_old_value, new_value) deserialize(raw_old_value) != new_value end end 
config/initalizers/types.rb
 ActiveRecord::Type.register(:encrypted, EncryptedType) 

Now you can specify this attribute as encrypted in the model:

 class User < ApplicationRecord attribute :name, :encrypted # If you have a lot of fields, you can use metaprogramming: %i[name phone address1 address2 ssn].each do |field_name| attribute field_name, :encrypted end end 

The name attribute will be transparently encrypted and decrypted during calls to the database. It also means that you can apply the same transformation to as many attributes as possible without rewriting the same code.

+6
source share

Why do you disclose it as a name in general?

 class User < ApplicationRecord def decrypted_name=(val) name = val.encrypt end def decrypted_name name.decrypt end end 

Then you use @model.decrypted_name instead of @model.name , since the name is encrypted and this is stored in the database.

 edit.haml =@user.decrypted _name =form_for @user, html: { multipart: true } do |f| =f.text_field :decrypted_name 

And name , if it is encrypted, should not be processed directly, but with this decrypted_name accessor.

+1
source share

I found this similar question: How do I / O methods (text_area, text_field, etc.) get attribute values ​​from an entry in a form_for block?

I added

  def name_before_type_cast (super() || '').decrypt end 

And now it works!

Here is the complete solution:

  @@encrypted_fields = [:name, :phone, :address1, :address2, :ssn, ...] @@encrypted_fields.each do |m| setter = (m.to_s+'=').to_sym getter = m getter_btc = (m.to_s+'_before_type_cast').to_sym define_method(setter) do |v| super v.encrypt end define_method(getter) do (super() || '').decrypt end define_method(getter_btc) do (super() || '').decrypt end end 

Some documents: http://api.rubyonrails.org/classes/ActiveRecord/AttributeMethods/BeforeTypeCast.html

+1
source share

All Articles