Search code examples
ruby-on-railsrubydatabaseformsvirtual-attribute

Rails virtual attribute in form not updating db from form or console


So in my Rails web application I have a section of code that prompts underage users to input a parent/guardian email when creating an account. In my database I would like to store this as the corresponding ID but naturally the user will want to enter an email.

this part of my form looks like this:

<%= f.label :guardian_email, "Guardian Email" %>
<%= f.text_field :guardian_email, class: 'form-control' %>

The corresponding code in my model looks like this:

def guardian_email
    User.find_by_id(guardian_id).email if guardian_id
end

def guardian_email=(g_email)
    guardian = User.find_by_email(g_email)
    print guardian.id
    self.guardian_id = guardian.id
    print self.guardian_id
end

(the print lines I've added while debugging) However, submitting the form does not update the desired attribute. I then tried from the console and typed:

User.find_by_email("[email protected]").guardian_email=("[email protected]")

I get the output:

User Load (0.5ms)  SELECT  "users".* FROM "users" WHERE "users"."email" = ? LIMIT ?  [["email", "[email protected]"], ["LIMIT", 1]]
User Load (0.3ms)  SELECT  "users".* FROM "users" WHERE "users"."email" = ? LIMIT ?  [["email", "[email protected]"], ["LIMIT", 1]]
88 => "[email protected]" 

but when I load the user afterwards the attribute guardian_id is still nil. what is odd is that both of the print statements return the proper value (in this case 8) but the assignment has never occured, despite the fact the second 8 that is printed out is the value "self.guardian_id".

I've followed railscasts #16 - virtual attributes to arrive at the code I have now, Thanks.

EDIT:

The reason the above code would not function is because I was permitting the actual attribute in the controller as opposed to the virtual one. Although as max pointed out several other bad practices are also present in this code.

Something I still do not understand though is that calling the guardian_email=() method in the console still does not update the attribute, but it works fine on the site.


Solution

  • Thats a very convoluted approach to a simple task.

    class User  < ApplicationRecord
      belongs_to :guardian,
        class_name: 'User',
        optional: true
    
      def guardian_email=(email)
        self.guardian ||= User.find_by_email(email)
        @guardian_email = email
      end
    
      def guardian_email
        @guardian_email || guardian.try(:email)
      end
    end
    
    class CreateUsers < ActiveRecord::Migration[5.0]
      def change
        create_table :users do |t|
          t.string :email
          # ...
          t.belongs_to :guardian, foreign_key: { to_table: :users }
          t.timestamps
        end
        add_index :users, :email, unique: true
      end
    end
    

    You'll also want a custom validation which kicks in if guardian_email is not blank which checks if the email actually belongs to a user.

    class User  < ApplicationRecord
      # ...
      validate :guardian_exists, unless: ->{ self.guardian_email.blank? }
    
      # ...
    
      private
    
      def guardian_exists
        errors.add(:guardian_email) unless guardian || User.find_by_email(guardian_email)
      end
    end