Rails "can't write unknown attribute"

Viewed 1696

I've been trying to solve this problem for the whole day, but I couldn't solve it.

I'm getting this error:

ActiveModel::MissingAttributeError in Users::RegistrationsController#add_data
can't write unknown attribute `[:drivlicense_nr, :birth_nation]`

user.rb:

class User < ApplicationRecord
  # Include default devise modules. Others available are:
  # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
  has_one :person
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :trackable, :validatable, :confirmable,
         :omniauthable, omniauth_providers: %i[facebook twitter google_oauth2]


  #validate :password_complexity

  private

  def password_complexity
    if password.present? && !password.match(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)./)
        errors.add :password, 'must include at least one lowercase letter, one uppercase letter, and one digit'
    end
  end

  def self.from_omniauth(auth)
    # Either create a User record or update it based on the provider (Google) and the UID
    where(email: auth.email, uid: auth.uid).first_or_create do |user|
      user.token = auth.credentials.token
      user.expires = auth.credentials.expires
      user.expires_at = auth.credentials.expires_at
      user.refresh_token = auth.credentials.refresh_token
      user.provider = auth.provider
      user.uid = auth.uid
      user.email = auth.info.email
      user.password = Devise.friendly_token[0,20]
      user.skip_confirmation!
      user.save!
    end
  end

    def self.new_with_session(params, session)
      super.tap do |user|
        if data = session['devise.facebook_data'] && session['devise.facebook_data']['extra']['raw_info']
            user.email = data['email'] if user.email.blank?
        end
        if data = session['devise.google_data'] && session['devise.google_data']['extra']['raw_info']
            user.email = data['email'] if user.email.blank?
        end
      end
    end
end

In person.rb I'm using custom primary_key since those are really important for my application.

class Person < ApplicationRecord
    belongs_to :user, optional: true
    has_many :cars
    self.primary_key = %i[drivlicense_nr birth_nation]

    VALID_FISCAL_CODE_REGEX = /\A^[A-Z]{6}[0-9]{2}[A-Z][0-9]{2}[A-Z][0-9]{3}[A-Z]$\z/
    validates :fiscal_code, presence: true, length: {is: 16}, format: { with: VALID_FISCAL_CODE_REGEX }
end

People migration:

class CreatePeople < ActiveRecord::Migration[5.2]
  def change
    create_table :people, primary_key: %i[drivlicense_nr birth_nation] do |t|
      t.integer     :usercode
      t.string      :email, null: false, default: ''
      t.string      :plate_nr, limit: 8, null: false, default: ''
      t.string      :drivlicense_nr, null: false, default: ''
      t.string      :fiscal_code, limit: 16
      t.string      :name
      t.string      :surname
      t.string      :phone_number
      t.date        :birth_date
      t.string      :birth_nation, limit: 2, null: false, default: 'IT'
      t.string      :birth_place
      t.string      :current_address
      t.string      :city
      t.string      :sex                       
      t.string      :region
      t.string      :zipcode, limit: 5
      t.string      :state, limit: 2
      t.timestamps  null: false
    end

    add_index :people, %i[drivlicense_nr birth_nation], name: 'index_people', unique: true
    add_index :people, :usercode,    name: 'index_people_on_usercode', unique: true
    add_index :people, :fiscal_code, unique: true
    # add_index :people, :pcode,                                                      unique: true
  end
end

People migration:

class DeviseCreateUsers < ActiveRecord::Migration[5.0]
  def change
      create_table :users do |t|
          t.integer     :usercode
          t.belongs_to  :person, index: true
          t.boolean     :admin, default: false
          t.string      :drivlicense_nr, null: false, default: ''
          t.string      :birth_nation, limit: 2, null: false, default: 'IT'
          t.string      :tpoliceman_id

          ## Database authenticatable
          t.string      :email
          t.string      :encrypted_password

          ## Recoverable
          t.string      :reset_password_token
          t.datetime    :reset_password_sent_at

          ## Rememberable
          t.datetime    :remember_created_at

          ## Trackable
          t.integer     :sign_in_count, null: false, default: 0
          t.datetime    :current_sign_in_at
          t.datetime    :last_sign_in_at
          t.string      :current_sign_in_ip
          t.string      :last_sign_in_ip

          ## Confirmable
          t.string      :confirmation_token
          t.datetime    :confirmed_at
          t.datetime    :confirmation_sent_at
          t.string      :unconfirmed_email # Only if using reconfirmable
          #t.datetime  :updated_at

          ## Omniauthable
          t.string      :provider
          t.string      :uid
          t.string      :refresh_token
          t.string      :token
          t.boolean     :expires
          t.integer     :expires_at

          ## Lockable
          t.integer     :failed_attempts,                      null: false, default: 0 # Only if lock strategy is :failed_attempts
          t.string      :unlock_token # Only if unlock strategy is :email or :both
          t.datetime    :locked_at

          t.timestamps  null: false
      end

      add_index :users, :usercode,             unique: true
      add_index :users, :email,                unique: true
      add_index :users, :reset_password_token, unique: true
      add_index :users, :confirmation_token,   unique: true
      add_index :users, :unlock_token,         unique: true
      #add_index :users, %i[drivlicense_nr birth_nation], name: 'index_users_on_person'#, unique: true
      # validates :drivlicense_nr, uniqueness: { scope: :birth_nation }
  end
end

What I'm trying to do here is after i sign-up through google/facebook, the user must fill another form. Now i need to also update the user to have the same value for drivlicense and birth_nation (my primary key). The error is triggered once the program do @person.save! Registrations_Controller#add_data:

def add_data
    @user = User.find_by_email(params[:email])
    if request.get?
      @person = Person.new()
      render 'oauth_add_data'
    elsif request.put?
        @person = Person.new(person_params)
        @user.update(drivlicense_nr: @person['drivlicense_nr'], birth_nation: @person['birth_nation'])
        if @person.save!
          flash[:success] = "Sign up process successful"
          bypass_sign_in(@user)
          redirect_to root_url
        else
          render 'new'
        end
    end
  end

Thanks in advance for your help and patience!

UPDATE schema.rb:

ActiveRecord::Schema.define(version: 2020_02_29_181024) do

  create_table "car_associated_person", options: "ENGINE=InnoDB DEFAULT CHARSET=utf8", force: :cascade do |t|
    t.string "plate_nr", limit: 8
    t.string "drivlicense_nr"
    t.string "birth_nation", limit: 2
    t.string "email"
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
  end

  create_table "cars", options: "ENGINE=InnoDB DEFAULT CHARSET=utf8", force: :cascade do |t|
    t.string "plate_nr", limit: 8, default: "", null: false
    t.string "chassis_nr", default: "", null: false
    t.string "owner_drivlicense_nr", default: "", null: false
    t.string "owner_birth_nation", limit: 2
    t.date "enrollment_date", default: "2018-01-01", null: false
    t.string "enrollment_nr", default: "", null: false
    t.string "enrollment_nation", default: "", null: false
    t.string "brand", default: "", null: false
    t.string "model", default: "", null: false
    t.integer "infraction_nr", default: 0
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
    t.index ["chassis_nr"], name: "index_cars_on_chassis_nr", unique: true
    t.index ["owner_birth_nation"], name: "index_cars_on_owner_birth_nation", unique: true
    t.index ["owner_drivlicense_nr"], name: "index_cars_on_owner_drivlicense_nr", unique: true
    t.index ["plate_nr"], name: "index_cars_on_plate_nr", unique: true
  end

  create_table "fines", options: "ENGINE=InnoDB DEFAULT CHARSET=utf8", force: :cascade do |t|
    t.bigint "car_id"
    t.integer "fine_nr", default: 0
    t.string "plate_nr", limit: 8, default: "", null: false
    t.datetime "fine_datetime", default: "2018-01-01 00:00:00", null: false
    t.string "fine_address", default: "", null: false
    t.string "infraction_article", limit: 10, default: "", null: false
    t.string "infraction_informcode"
    t.string "infraction_motivation", default: "", null: false
    t.integer "deduction_points", default: 0
    t.float "fine_amount_reduced", default: 0.0
    t.float "procedures_amount", default: 0.0
    t.float "fine_total_amount", default: 0.0
    t.integer "days_nr_payment", limit: 1, default: 5
    t.string "paid", default: "0"
    t.string "optional_penalty", default: "None"
    t.string "tpoliceman_idnr_1"
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
    t.index ["car_id"], name: "index_fines_on_car_id"
    t.index ["fine_nr"], name: "index_fines_on_fine_nr", unique: true
    t.index ["infraction_informcode"], name: "index_fines_on_infraction_informcode", unique: true
    t.index ["plate_nr"], name: "index_fines_on_plate_nr", unique: true
  end

  create_table "people", options: "ENGINE=InnoDB DEFAULT CHARSET=utf8", force: :cascade do |t|
    t.integer "usercode"
    t.string "email", default: "", null: false
    t.string "plate_nr", limit: 8, default: "", null: false
    t.string "drivlicense_nr", default: ""
    t.string "fiscal_code", limit: 16
    t.string "name"
    t.string "surname"
    t.string "phone_number"
    t.date "birth_date"
    t.string "birth_nation", limit: 2, default: "IT"
    t.string "birth_place"
    t.string "current_address"
    t.string "city"
    t.string "sex"
    t.string "region"
    t.string "zipcode", limit: 5
    t.string "state", limit: 2
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
    t.index ["fiscal_code"], name: "index_people_on_fiscal_code", unique: true
    t.index ["usercode"], name: "index_people_on_usercode", unique: true
  end

  create_table "users", id: :integer, options: "ENGINE=InnoDB DEFAULT CHARSET=utf8", force: :cascade do |t|
    t.integer "usercode"
    t.integer "person_id"
    t.boolean "admin", default: false
    t.string "drivlicense_nr", default: "", null: false
    t.string "birth_nation", limit: 2, default: "IT", null: false
    t.string "tpoliceman_id"
    t.string "email"
    t.string "encrypted_password"
    t.string "reset_password_token"
    t.datetime "reset_password_sent_at"
    t.datetime "remember_created_at"
    t.integer "sign_in_count", default: 0, null: false
    t.datetime "current_sign_in_at"
    t.datetime "last_sign_in_at"
    t.string "current_sign_in_ip"
    t.string "last_sign_in_ip"
    t.string "confirmation_token"
    t.datetime "confirmed_at"
    t.datetime "confirmation_sent_at"
    t.string "unconfirmed_email"
    t.string "provider"
    t.string "uid"
    t.string "refresh_token"
    t.string "token"
    t.boolean "expires"
    t.integer "expires_at"
    t.integer "failed_attempts", default: 0, null: false
    t.string "unlock_token"
    t.datetime "locked_at"
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
    t.index ["confirmation_token"], name: "index_users_on_confirmation_token", unique: true
    t.index ["email"], name: "index_users_on_email", unique: true
    t.index ["person_id"], name: "index_users_on_person_id"
    t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true
    t.index ["unlock_token"], name: "index_users_on_unlock_token", unique: true
    t.index ["usercode"], name: "index_users_on_usercode", unique: true
  end

end
1 Answers

Rails uses the id field as a surrogate key it then enforces uniqueness on that field in the application and database layers. This way you don't have to worry about creating composite primary keys (as in school).

Rails relies on a lot of convention over configuration, primary keys being just one of those things. You could configure any primary key you like, but Rails will fight you especially if you want to use a composite key. There is a gem to help you do it if you really need to, but I'd stay away from that as it introduces complexity.

Related