Can nested attributes be used in combination with inheritance? - ruby-on-rails

I have the following classes:
Project
Person
Person > Developer
Person > Manager
In the Project model I have added the following statements:
has_and_belongs_to_many :people
accepts_nested_attributes_for :people
And of course the appropriate statements in the class Person. How can I add a Developer to a Project through the nested_attributes method? The following does not work:
#p.people_attributes = [{:name => "Epic Beard Man", :type => "Developer"}]
#p.people
=> [#<Person id: nil, name: "Epic Beard Man", type: nil>]
As you can see the type attributes is set to nil instead of "Developer".

Solution for Rails3: attributes_protected_by_default in now a class-method:
class Person < ActiveRecord::Base
private
def self.attributes_protected_by_default
super - [inheritance_column]
end
end

I encountered a similar problem few days ago. The inheritance column(i.e. type) in a STI model is a protected attribute. Do the following to override the default protection in your Person class.
Rails 2.3
class Person < ActiveRecord::Base
private
def attributes_protected_by_default
super - [self.class.inheritance_column]
end
end
Rails 3
Refer to the solution suggested by #tokland.
Caveat:
You are overriding the system protected attribute.
Reference:
SO Question on the topic

Patches above did not work for me, but this did (Rails3):
class ActiveRecord::Reflection::AssociationReflection
def build_association(*options)
if options.first.is_a?(Hash) and options.first[:type].presence
options.first[:type].to_s.constantize.new(*options)
else
klass.new(*options)
end
end
end
Foo.bars.build(:type=>'Baz').class == Baz

For those of us using Mongoid, you will need to make the _type field accessible:
class Person
include Mongoid::Document
attr_accessible :_type
end

Related

Rails make model methods appear as attributes

I have a model connecting to a Postgres db.
class Person < ApplicationRecord
def say_id
"#{name} has id: #{id}"
end
end
I have some attributes id,name,email as well as the method above: say_id that can be accessed via:
person = Person.new
person.id => 1
person.say_id => "John has id: 1"
I would like to have the method 'say_id' listed as an attribute as well, now when running person.attributes, I'm only seeing: id, name, email
How can I have my method included as a listable information in full, as with person.attributes but which will include my method? A usecase would be for lazily just laying out all these fields in a table of the Person-object.
In Rails 5+ you can use the attributes api to create attributes that are not backed by a database column:
class Person < ApplicationRecord
attribute :foo
end
irb(main):002:0> Person.new.attributes
=> {"id"=>nil, "email"=>nil, "name"=>nil, "created_at"=>nil, "updated_at"=>nil, "foo"=>nil}
Unlike if you used attr_accessor these actually behave very much like database backed attributes.
You can then override the getter method if you wanted to:
class Person < ApplicationRecord
attribute :foo
def foo
"foo is #{super}"
end
end
irb(main):005:0> Person.new(foo: 'bar').foo
=> "foo is bar"
But for whatever you're doing its still not the right answer. You can get a list of the methods of an class by calling .instance_methods on a class:
irb(main):007:0> Person.instance_methods(false)
=> [:foo]
Passing false filters out inherited methods.

How to handle rails enums?

How can I handle enums in rails? I have googled this, but not finding any clean solutions. So far I have come up with including the concern below on models that use interaction_type_id. This is my foreign key to my enum table in the database. Using this approach I don't have to use ids all over my code, but when saving an object that relates to an interact_type I can say
myobject.interaction_type = :file_download
This can then persist the the database with the correct id since the concern(see concern below - included on models that use the enum) will return the correct id.
module InteractionTypeEnum
extend ActiveSupport::Concern
included do
INTERACTION_TYPE = { file_download: 1, email: 2, telesales: 3, registration: 4, enrolment: 5 }
end
def interaction_type
INTERACTION_TYPE.key(read_attribute(:interaction_type_id)).to_s.gsub('_',' ').capitalize
end
def interaction_type=(s)
write_attribute(:interaction_type_id, INTERACTION_TYPE[s])
end
end
This just feels heavy. There must be an easier/cleaner way. Now when trying to write tests for this it gets even more messy.
Most of the reasons for wanting my enums in code and database are performance (code) and reporting (database).
Any help appreciated. Thanks.
I recommend the active_enum gem.
Example from their docs, if you have an integer column called sex on the class User:
class User < ActiveRecord::Base
enumerate :sex do
value :name => 'Male'
value :name => 'Female'
end
end
Or you can define the enum in a seperate class:
class Sex < ActiveEnum::Base
value 1 => 'Male'
value 2 => 'Female'
end
class User < ActiveRecord::Base
enumerate :sex, :with => Sex
end
I like the abstraction it provides, and it saves you from having to create an entire database table just to store your enum values.
I use the following method, say I have
class PersonInfo < ActiveRecord::Base
belongs_to :person_info_type
end
and PersonInfoType is a simple domain table, containing the possible types of information.
Then I code my model as follows:
class PersonInfoType < ActiveRecord::Base
PHONE = 1
EMAIL = 2
URL = 3
end
I have a seed fills the database with the corresponding data.
And so when assigning some person-information can do something like
person.person_infos << PersonInfo.create(:info => 'http://www.some-url.com', :person_info_type_id => PersonInfoType::URL)
This code can then be further cleaned up using relations:
class PersonInfo
belongs_to :person_info_type
def self.phones
PersonInfo.where(:person_info_type_id => PersonInfoType::PHONE)
end
end
person.person_infos << PersonInfo.phones.create(:info => '555 12345')

Ruby copying between active model objects

Simply trying to work out how to copy attributes from one Active Model to another without having to do it one by one.
I have two models one is RFM (ruby to filemaker) one is mongoid both mixin active model.
gem "ginjo-rfm"
the model
require 'rfm'
class StudentAdmin < Rfm::Base
config :layout => 'STUDENT_ADMIN_LAYOUT'
attr_accessor :name_first,:name_last
end
Mongoid model
class Student
include Mongoid::Document
field :first_name, type: String
field :last_name, type: String
end
Is there a quicky copy I can do? I found a sample between active record objects e.g.
student_admin = ... #load StudentAdmin
Student.new(student_admin.attributes.slice(Student.attribute_names))
but RFM doesn't provide a attributes method.
EDIT
Sorry what I am trying to achive is a better way than this
student_admins = #get student admins from external service
students = []
student_admins.each() do |sa|
students.push(Student.create!(first_name: sa.name_first, last_name: sa.name_last))
end
This example only shows 2 attributes, but in practice there is over 50 and was wondering if there is a way to do it without having to specify every attribute e.g. if the attribute names are the same on two objects copy them automatically.
Try this:
students = student_admins.map do |sa|
attrs = sa.methods.inject({}) do |hash, m|
next unless Student.column_names.include? m.to_s
hash[m] = sa.send m
end
Student.create(attrs)
end
Student would have to be a class that inherits from ActiveRecord::Base:
class Student < ActiveRecord::Base
...
end

Mongoid: cannot embed different classes in one array

I have Mongoid classes as follows:
class Order
include Mongoid::Document
embeds_many :animals
end
class Animal
include Mongoid::Document
embedded_in :order
def self.has_gender
field :gender, type: String
end
end
class Deer < Animal
has_gender
end
and when I call animals on any order, even empty one:
Order.new.animals
I get the following error:
undefined method `has_gender' for Deer:Class
Any ideas?
The problem is somewhere else. Your code works on my machine. (I'm using Mongoid 3.0-rc, though).
order = Order.new
order.animals << Animal.new
order.animals << Deer.new
order.save
puts Order.first.animals
# >> #<Animal:0x007fca04bae890>
# >> #<Deer:0x007fca04bb4b50>
I think that the problem is in the way I create sub-classes:
class Game
include Mongoid::Document
TYPES = {'deer' => Deer, 'pig' => Pig, 'duck' => Duck}
def self.new_of_type(type, attrs={})
TYPES[type].new attrs
end
end
because when I commented out line when I define TYPES, error disappeared, so the problem may be with calling subclasses when defining TYPES (Deer, Pig, Duck).
Any ideas for a better solution for creating sub-classes? i'm doing it this way in controller:
class GamesController < ApplicationController
def create
#game = Game.new_of_type params[:type], params[:game]
#game.save
end
end

Rails attr_accessible does not work for :type?

Im trying set the single table inheritance model type in a form. So i have a select menu for attribute :type and the values are the names of the STI subclasses. The problem is the error log keeps printing:
WARNING: Can't mass-assign these protected attributes: type
So i added "attr_accessible :type" to the model:
class ContentItem < ActiveRecord::Base
# needed so we can set/update :type in mass
attr_accessible :position, :description, :type, :url, :youtube_id, :start_time, :end_time
validates_presence_of :position
belongs_to :chapter
has_many :user_content_items
end
Doesn't change anything, the ContentItem still has :type=nil after .update_attributes() is called in the controller. Any idea how to mass update the :type from a form?
we can override attributes_protected_by_default
class Example < ActiveRecord::Base
def self.attributes_protected_by_default
# default is ["id","type"]
["id"]
end
end
e = Example.new(:type=>"my_type")
You should use the proper constructor based on the subclass you want to create, instead of calling the superclass constructor and assigning type manually. Let ActiveRecord do this for you:
# in controller
def create
# assuming your select has a name of 'content_item_type'
params[:content_item_type].constantize.new(params[:content_item])
end
This gives you the benefits of defining different behavior in your subclasses initialize() method or callbacks. If you don't need these sorts of benefits or are planning to change the class of an object frequently, you may want to reconsider using inheritance and just stick with an attribute.
Duplex at railsforum.com found a workaround:
use a virtual attribute in the forms
and in the model instead of type
dirtectly:
def type_helper
self.type
end
def type_helper=(type)
self.type = type
end
Worked like a charm.
"type" sometimes causes troubles... I usually use "kind" instead.
See also: http://wiki.rubyonrails.org/rails/pages/ReservedWords
I followed http://coderrr.wordpress.com/2008/04/22/building-the-right-class-with-sti-in-rails/ for solving the same problem I had. I'm fairly new to Rails world so am not so sure if this approach is good or bad, but it works very well. I've copied the code below.
class GenericClass < ActiveRecord::Base
class << self
def new_with_cast(*a, &b)
if (h = a.first).is_a? Hash and (type = h[:type] || h['type']) and (klass = type.constantize) != self
raise "wtF hax!!" unless klass < self # klass should be a descendant of us
return klass.new(*a, &b)
end
new_without_cast(*a, &b)
end
alias_method_chain :new, :cast
end
class X < GenericClass; end
GenericClass.new(:type => 'X') # => #<X:0xb79e89d4 #attrs={:type=>"X"}>

Resources