Changing types in single inheritance

I’m trying to write an application that involves with three kinds of
inherited models:

The lowest is Person, a model merely used as records, and nothing else
of importance.
Officer inherits from Person, and unlike Person, stores a password for
verification and logging in.
Admin inherits from Officer, and is the same as Officers except there
must be at least one of them.

I want to make an internal method that allows me to turn a Person into
an Officer, an Officer to Admin, Admin to Person, etc. What’s a clean
method of upgrading/downgrading within this single-inheritance?

Here’s the source file, as well as a quick template for what I want to
see happen:
=======Person.rb========
class Person < ActiveRecord::Base
#first_name is required: must start with a capital
validates_presence_of :first_name
validates_format_of :first_name,
:with => /^[A-Z][a-zA-Z0-9, .]+$/

#last_name is required: must start with a capital
validates_presence_of :last_name
validates_format_of :last_name,
:with => /^[A-Z][a-zA-Z0-9, .]+$/

#rin is required: must be unique; also, must all be lowercases
#with 0 to 2 numbers following the letters.
validates_presence_of :rin
validates_format_of :rin,
:with => /^[a-z]+[0-9]{0,2}$/
validates_uniqueness_of :rin

#email must be unique, and it is required.
validates_presence_of :email
validates_format_of :email,
:with => /^[a-zA-Z0-9_]+@[a-zA-Z0-9_]+(.[a-z]{2,3}){1,2}$/
validates_uniqueness_of :email

#year is required, and must be a 4 digit number greater than 2000
validates_presence_of :year
validates_numericality_of :year,
:only_integer => true
validates_length_of :year,
:is => 4

#turns a Person or Officer to Admin
def turn_to_admin
self[:type] = ‘Admin’
#somehow update with Admin-based validations
self
end

#turns a Person or Admin to Officer
def turn_to_officer
self[:type] = ‘Officer’
#somehow update with Officer-based validations
self
end

#technically, this does nothing
def turn_to_person
nil
end

protected

def validate
errors.add(:year, “should be between 2000 and 3000”) if
( year.to_i < 2000 or year.to_i > 3000 )
end

#completely ignore the passwords
end
=======Officer.rb========
class Officer < Person
#take care of password thingy
validates_length_of :password, :minimum => 5

attr_accessor :password_confirmation
validates_confirmation_of :password

#some functions
def validate
errors.add_to_base(“Missing password”) if hashed_password.blank?
end

def self.authenticate(name, password)
person = self.find_by_rin(name)
if person
expected_password = encrypted_password(password, person.salt)
if person.hashed_password!=expected_password
person = nil
end
end
person
end

def password
@password
end

def password=(pwd)
@password = pwd
create_new_salt
self.hashed_password = Officer.encrypted_password(self.password,
self.salt)
end

#turn_to_admin is inherited from Person

#This does nothing
def turn_to_officer
nil
end

#turns an Officer or Admin
def turn_to_person
self[:type] = ‘Person’
#somehow update with Officer-based validations
self
end

#careful of private statements
private

def self.encrypted_password(password, salt)
string_to_hash = password + “Japan is an island of interest” +
salt
Digest::SHA1.hexdigest(string_to_hash)
end

def create_new_salt
self.salt = (self.object_id * rand).to_s + rand.to_s
end
end
=======Admin.rb========
class Admin < Officer

#Make sure there’s at least one admin
def after_destroy
if Admin.count.zero?
raise “Can’t delete the last admin”
end
end

#turn_to_person is inherited by Officer

#Return nil, again
def turn_to_admin
nil
end

#turns an Admin to Officer
def turn_to_officer
self[:type] = ‘Officer’
#somehow update with Officer-based validations
self
end
end

Single table inheritance applies perfectly to this need,
basically you have to add a type column,
and build the people table with all the columns that any of the three
classes will do,
then both admin and officer have to inherit from person,
(I never tested double inheritance)
so some repetition at admin and officer model may happen,
(you can use a mixin if you want to avoid that)

I think he’s got this already.

What he’s trying to do is assign a person to be an Admin or an Officer.
I
think all you do is self.type = "Admin and then self.reload and it might
work.

On Tue, May 27, 2008 at 12:28 PM, Andres [email protected] wrote:

validates_format_of :first_name,
validates_format_of :rin,
validates_presence_of :year
end
nil
end
errors.add_to_base(“Missing password”) if hashed_password.blank?
person
self.salt)
def turn_to_person
salt
#Make sure there’s at least one admin
nil


Appreciated my help?
Recommend me on Working With Rails
http://workingwithrails.com/person/11030-ryan-bigg

Yeah, I did already know. It’s is why I was asking something
else :-)!
Anyway, I tried the reload method:

def test_changing_officer_to_member
officer = Officer.find(:first)
assert officer
assert officer.update_attributes(:type => ‘Person’,
:password => ‘Ha, ha, you cannot login!!!’,
:password_confirmation => ‘Ha, ha, you cannot login!!!’)
officer.reload
assert_equal officer.class.to_s, ‘Person’
end

It failed miserably:

  1. Failure:
    test_changing_officer_to_member(OfficerTest)
    [officer_test.rb:89:in test_changing_officer_to_member' c:/ruby/lib/ruby/gems/1.8/gems/activesupport-2.0.2-/lib/ active_support/testing/default.rb:7:in run’]:
    <“Officer”> expected but was
    <“Person”>.

9 tests, 22 assertions, 1 failures, 0 errors
Process ruby exited with code 1

Any other solutions?

In addition, looking back at my test code, I noticed that:

def test_defective_new
officer = Officer.new( :first_name => ‘John’,
:last_name => ‘Doe’, :rin => ‘doej’,
:email => ‘[email protected]’, :year => 2011,
:password => ‘forka’)
assert officer.valid?
assert_equal officer.class.to_s, ‘Officer’
end

Passes. It shouldn’t. It should compare whether password and
confirmation password is the same thing, then give out an error. Why
by only giving a password, the model passes? After all, this test
passes correctly:

def test_unequal_password_confirmation
officer = Officer.new( :first_name => ‘John’,
:last_name => ‘Doe’, :rin => ‘doej’,
:email => ‘[email protected]’, :year => 2011,
:password => ‘jojoj’, :password_confirmation => ‘kkkkk’)
assert !officer.valid?
assert officer.errors.invalid?(:password)
end

Finally, random, but I’ll also like to test in “rake script/console
testing”. Unfortunately, the console doesn’t seem to realize I’ve
downloaded the plugin “acts_as_list”, nor does it seem to connect the
Database really well. What can I do to fix this? I’m using
InstantRails, by the way: does this have to do with anything?

On May 27, 12:26 am, “Ryan B. (Radar)” [email protected]

Reinitialize the variable seems to be the only option then, switch the
type
over to Officer and then do Officer.find(@person.id) and it may work
that
way.

On Wed, May 28, 2008 at 9:47 AM, Taro [email protected] wrote:

 :password_confirmation => 'Ha, ha, you cannot login!!!')

active_support/testing/default.rb:7:in `run’]:
def test_defective_new
by only giving a password, the model passes? After all, this test

What he’s trying to do is assign a person to be an Admin or an Officer. I

classes will do,
else
clean

:with => /^[A-Z][a-zA-Z0-9, .]+$/
:with => /^[a-z]+[0-9]{0,2}$/

validates_numericality_of :year,

end
=======Officer.rb========
end
end
end
self[:type] = ‘Person’
Digest::SHA1.hexdigest(string_to_hash)
def after_destroy
end
Appreciated my help?
Recommend me on Working With Railshttp://
workingwithrails.com/person/11030-ryan-bigg


Appreciated my help?
Recommend me on Working With Rails
http://workingwithrails.com/person/11030-ryan-bigg

Behold!!!


def test_changing_officer_to_member
officer = Officer.find(:first)
assert officer
assert officer.update_attributes(:type => ‘Person’,
:password => ‘Ha, ha, you cannot login!!!’,
:password_confirmation => ‘Ha, ha, you cannot login!!!’)
officer.reload
assert_equal officer.class.to_s, ‘Person’
end

def test_changing_officer_to_member2
officer = Officer.find(:first)
assert officer
officer.type = ‘Person’
officer.reload
assert_equal officer.class.to_s, ‘Person’
end

def test_changing_officer_to_member3
officer = Officer.find(:first)
assert officer
officer[:type] = ‘Person’
officer.reload
assert_equal officer.class.to_s, ‘Person’
end

def test_changing_officer_to_member4
officer = Officer.find(:first)
assert officer
officer[:type] = ‘Person’
officer = Officer.find(officer.id)
assert_equal officer.class.to_s, ‘Person’
end

def test_changing_officer_to_member5
officer = Officer.find(:first)
assert officer
officer[:type] = ‘Person’
officer = Person.find(officer.id)
assert_equal officer.class.to_s, ‘Person’
end

def test_changing_officer_to_member6
officer = Officer.find(:first)
assert officer
officer.type = ‘Person’
officer = Person.find(officer.id)
assert_equal officer.class.to_s, ‘Person’
end

  1. Failure:
    test_changing_officer_to_member(OfficerTest)
    [officer_test.rb:90:in test_changing_officer_to_member' c:/ruby/lib/ruby/gems/1.8/gems/activesupport-2.0.2-/lib/ active_support/testing/default.rb:7:in run’]:
    <“Officer”> expected but was
    <“Person”>.

  2. Failure:
    test_changing_officer_to_member2(OfficerTest)
    [officer_test.rb:98:in test_changing_officer_to_member2' c:/ruby/lib/ruby/gems/1.8/gems/activesupport-2.0.2-/lib/ active_support/testing/default.rb:7:in run’]:
    <“Officer”> expected but was
    <“Person”>.

  3. Failure:
    test_changing_officer_to_member3(OfficerTest)
    [officer_test.rb:106:in test_changing_officer_to_member3' c:/ruby/lib/ruby/gems/1.8/gems/activesupport-2.0.2-/lib/ active_support/testing/default.rb:7:in run’]:
    <“Officer”> expected but was
    <“Person”>.

  4. Failure:
    test_changing_officer_to_member4(OfficerTest)
    [officer_test.rb:114:in test_changing_officer_to_member4' c:/ruby/lib/ruby/gems/1.8/gems/activesupport-2.0.2-/lib/ active_support/testing/default.rb:7:in run’]:
    <“Officer”> expected but was
    <“Person”>.

  5. Failure:
    test_changing_officer_to_member5(OfficerTest)
    [officer_test.rb:122:in test_changing_officer_to_member5' c:/ruby/lib/ruby/gems/1.8/gems/activesupport-2.0.2-/lib/ active_support/testing/default.rb:7:in run’]:
    <“Officer”> expected but was
    <“Person”>.

  6. Failure:
    test_changing_officer_to_member6(OfficerTest)
    [officer_test.rb:130:in test_changing_officer_to_member6' c:/ruby/lib/ruby/gems/1.8/gems/activesupport-2.0.2-/lib/ active_support/testing/default.rb:7:in run’]:
    <“Officer”> expected but was
    <“Person”>.

On May 27, 8:19 pm, “Ryan B. (Radar)” [email protected]

Your test is wrong. You should be doing:

person = Person.find(id)
person.type = “Officer”
person.save
officer = Officer.find(id)
officer.class == Officer

And that will return true.

On Wed, May 28, 2008 at 9:55 AM, Taro [email protected] wrote:

officer.reload

assert officer
assert_equal officer.class.to_s, ‘Person’

test_changing_officer_to_member2(OfficerTest)
active_support/testing/default.rb:7:in `run’]:

[officer_test.rb:130:in `test_changing_officer_to_member6’
that

def test_changing_officer_to_member


assert_equal officer.class.to_s, ‘Officer’
:email => ‘[email protected]’, :year => 2011,

On Tue, May 27, 2008 at 12:28 PM, Andres [email protected] wrote:

for

validates_uniqueness_of :rin
validates_numericality_of :year,

end
=======Officer.rb========
hashed_password.blank?
end
self.hashed_password =

def self.encrypted_password(password, salt)
=======Admin.rb========

end
workingwithrails.com/person/11030-ryan-bigg


Appreciated my help?
Recommend me on Working With Rails
http://workingwithrails.com/person/11030-ryan-bigg

Oh, whoops.

Anyway, it did work. I’m curious about why "person.type = " works. I
read somewhere that the correct syntax should be "person[:type] = ",
due to the fact that “.type” is a (deprecated) class method.

On May 27, 9:33 pm, “Ryan B. (Radar)” [email protected]