Use nested attributes and hierarchies - ruby-on-rails

I have 2 tables is
- Training_Assessments : id, name
- Standards: id, name, training_assessment_id, parent_id
And Model Standards
class Standard < ApplicationRecord
ATTRIBUTE_PARAMS = %i(id name proportion parent_id).freeze
belongs_to :training_assessment
has_closure_tree
end
Model TrainingAssessment
class TrainingAssessment < ApplicationRecord
ATTRIBUTE_PARAMS =
[
:id, :name,
standards_attributes: Standard::ATTRIBUTE_PARAMS
].freeze
has_many :standards, dependent: :destroy
accepts_nested_attributes_for :standards, allow_destroy: true
end
I one training assessments have many standards, and each standard have many child standard have parent_id of parent standard. I try created at console but child standard don't have id of training assessments nested.
So how i can do it ? And how is the params structure?

You need an association from one standard to it's parent standard in your model:
belongs_to :parent_standard, foreign_key: :parent_id
Then you can delegate:
delegate :training_assessment, to: :parent_standard
EDIT:
Using closure_tree gem: child.root.training_assessment_id is the parents training_assessment_id

Related

Rails has_one association with multiple primary keys

Ruby 2.6.5 on Rails 5.2.4.1
Current Situation
I've got a table of Products, and I'd like to associate products as compatible with one another. For example, Product 1 is compatible with Product 2.
When creating the first ProductCompatibility, I populate it as follows:
#<ProductLibrary::ProductCompatibility id: 1, product_id: 1, compatible_product_id: 2>
At the moment, I can perform the following:
0> ProductLibrary::Product.find(1).compatible_products
=> #<ActiveRecord::Associations::CollectionProxy [#<ProductLibrary::Product id: 2>]
But I would also like to be able to perform the following, without creating an additional record:
0> ProductLibrary::Product.find(2).compatible_products
=> #<ActiveRecord::Associations::CollectionProxy [#<ProductLibrary::Product id: 1>]
Currently, the above returns the following:
0> ProductLibrary::Product.find(2).compatible_products
=> #<ActiveRecord::Associations::CollectionProxy [#<ProductLibrary::Product id: 2>]
Current Code
My models look like this:
module ProductLibrary
class Product < ApplicationRecord
has_many :product_compatibilities, ->(p) {
unscope(where: :product_id)
.where(product_id: p.id)
.or(ProductLibrary::ProductCompatibility.where(compatible_product_id: p.id))
}
has_many :compatible_products, through: :product_compatibilities
end
end
module ProductLibrary
class ProductCompatibility < ApplicationRecord
belongs_to :product
has_one :compatible_product,
primary_key: :compatible_product_id,
foreign_key: :id,
class_name: 'ProductLibrary::Product'
end
end
Intention
The primary_key in compatible_product is why I'm getting Product 2 when I request Product 2's compatible products (instead of Product 1).
What I'd like is for the has_one :compatible_product association to return products where the primary key is both :compatible_product_id and :product_id, but I can't figure out how to do that without writing multiple associations and compiling them in a helper method (which feels clunky and unconventional).
I'm not even sure it's possible, but it seems like it's along the lines of a
ProductLibrary::Product.where(id: [:product_id, :compatible_product_id])
which I couldn't get to work as association logic.
You should be using belongs_to instead of has_one.
module ProductLibrary
class ProductCompatibility < ApplicationRecord
belongs_to :product
belongs_to :compatible_product,
class_name: 'ProductLibrary::Product'
end
end
The semantics of has_one and belongs_to are a really common source of confusion but the difference is with belongs_to the foreign key column is on this models table and with has_one the FKC is on the other model.
What you are creating here is really just a join table and join model with the slight difference that both foreign keys happen to point to the same table instead of two different tables.
Here's what I ended up with, thanks to some help from #max
module ProductLibrary
class Product < ApplicationRecord
has_many :product_compatibilities, ->(p) {
unscope(where: :product_id)
.where(product_id: p.id)
.or(ProductLibrary::ProductCompatibility.where(compatible_product_id: p.id))
}
has_many :compatible_products, through: :product_compatibilities
has_many :inverse_compatible_products, through: :product_compatibilities
def all_compatible
(self.compatible_products + self.inverse_compatible_products).uniq.sort - [self]
end
end
end
module ProductLibrary
class ProductCompatibility < ApplicationRecord
belongs_to :product
belongs_to :compatible_product,
class_name: 'ProductLibrary::Product'
belongs_to :inverse_compatible_product,
foreign_key: :product_id,
class_name: 'ProductLibrary::Product'
end
end
I'll probably rename some things, and we may need to implement a boolean to drive whether a product can be compatible with itself (for now I assume not).
It's kind of what I was trying to avoid, but it looks like this is a correct solution.

Confusion with has_many :through

I am completely confused with implementing has_many :through for the dataset I am using. So I have two tables - scheme_master and scheme_detail
scheme_master has these fields - id, scheme_detail_id, primary_scheme_id
scheme_detail has one relevant field - id
Every scheme in scheme_master has a primary scheme which is self referential to the scheme_master table. For instance, scheme 1 is the primary_scheme of schemes 1,2,3.
Relevant codes are as below
scheme_master.rb
class SchemeMaster < ActiveRecord::Base
has_one :scheme_detail
has_many :child_schemes, class_name: "SchemeMaster",
foreign_key: :primary_scheme_id, primary_key: :id
end
scheme_detail.rb
class SchemeDetail < ActiveRecord::Base
belongs_to :scheme_master
end
My question is how do I access Scheme Details of all my child schemes?
Currently,
SchemeMaster.find(1).child_schemes
gives me all the child schemes - 1,2,3, but I want an association which would refer to scheme_detail of the child_schemes. Thank you.
Firstly, scheme_masters table has scheme_detail_id, so you need to change this association
class SchemeMaster < ActiveRecord::Base
belongs_to :scheme_detail
end
class SchemeDetail < ActiveRecord::Base
has_one :scheme_master
end
Now, to fetch all scheme_detail of the child_schemes, do this
scheme_master = SchemeMaster.includes(child_schemes: :scheme_detail).where(id: 1).first
To fetch scheme_detail of child_schemes, you can do, scheme_master.child_schemes.first.scheme_detail.
Hope that helps!

rails - belongs_to any different model

I have some models in my database:
- customer
has_many documents
- charts
has_many documents
- pages
has_many documents
Any of models above can have many documents.
How can I do this in the Document model? Is there any relationship can accept different models?
Yes, it is possible. This concept is called polymorphic association and can be done like this using Ruby on Rails:
class Document < ActiveRecord::Base
belongs_to :owner, polymorphic: true
class Customer < ActiveRecord::Base
has_many :documents, as: :owner
It uses 2 columns to work: one column to save the owner's type, and a second column to save th owner's id:
Document.create(owner_type: 'Customer', owner_id: customer.id)
Then, you can call the method .owner on the document object:
doc = Document.first
doc.owner # => Can either return a Customer, Chart or Page record
You might want to add some security around this, something to prevent from creating documents for a owner that is not supposed to have this relation:
class Document < ActiveRecord::Base
belongs_to :owner, polymorphic: true
validates :owner_type, inclusion: { in: %w( Customer Chart Page ) }
This will prevent from creating documents like this:
Document.create(owner_type: 'kittyCat', owner_id: 77) # won't work

How to create an association that sets join table attributes automatically?

I am totally confused about how I should go about "the rails way" of effectively using my associations.
Here is an example model configuration from a Rails 4 app:
class Film < ActiveRecord::Base
# A movie, documentary, animated short, etc
has_many :roleships
has_many :participants, :through => :roleships
has_many :roles, :through => :roleships
# has_many :writers........ ?
end
class Participant < ActiveRecord::Base
# A human involved in making a movie
has_many :roleships
end
class Role < ActiveRecord::Base
# A person's role in a film. i.e. "Writer", "Actor", "Extra" etc
has_many :roleships
end
class Roleship < ActiveRecord::Base
# The join for connecting different people
# to the different roles they have had in
# different films
belongs_to :participant
belongs_to :film
belongs_to :role
end
Given the above model configuration, the code I wish I had would allow me to add writers directly to a film and in the end have the join setup correctly.
So for example, I'd love to be able to do something like this:
## The Code I WISH I Had
Film.create!(name: "Some film", writers: [Participant.first])
I'm not sure if I'm going about thinking about this totally wrong but it seems impossible. What is the right way to accomplish this? Nested resources? A custom setter + scope? Something else? Virtual attributes? thank you!
I created a sample app based on your question.
https://github.com/szines/hodor_filmdb
I think useful to setup in Participant and in Role model a through association as well, but without this will work. It depends how would you like to use later this database. Without through this query wouldn't work: Participant.find(1).films
class Participant < ActiveRecord::Base
has_many :roleships
has_many :films, through: :roleships
end
class Role < ActiveRecord::Base
has_many :roleships
has_many :films, through: :roleships
end
Don't forget to give permit for extra fields (strong_parameters) in your films_controller.rb
def film_params
params.require(:film).permit(:title, :participant_ids, :role_ids)
end
What is strange, that if you create a new film with a participant and a role, two records will be created in the join table.
Update:
You can create a kind of virtual attribute in your model. For example:
def writers=(participant)
#writer_role = Role.find(1)
self.roles << #writer_role
self.participants << participant
end
and you can use: Film.create(title: 'The Movie', writers: [Participant.first])
If you had a normal has_and_belongs_to_many relationship i.e. beween a film and a participant, then you can create a film together with your examples.
As your joining model is more complex, you have to build the roleships separately:
writer= Roleship.create(
participant: Participant.find_by_name('Spielberg'),
role: Role.find_by_name('Director')
)
main_actor= Roleship.create(
participant: Participant.find_by_name('Willis'),
role: Role.find_by_name('Actor')
)
Film.create!(name: "Some film", roleships: [writer, main_actor])
for that, all attributes you use to build roleships and films must be mass assignable, so in a Rails 3.2 you would have to write:
class Roleship < ActiveRecord::Base
attr_accessible :participant, :role
...
end
class Film < ActiveRecord::Base
attr_accessible :name, :roleships
...
end
If you want to user roleship_ids, you have to write
class Film < ActiveRecord::Base
attr_accessible :name, :roleship_ids
...
end
Addendum:
Of cause you could write a setter method
class Film ...
def writers=(part_ids)
writer_role=Role.find_by_name('Writer')
# skiped code to delete existing writers
part_ids.each do |part_id|
self.roleships << Roleship.new(role: writer_role, participant_id: part_id)
end
end
end
but that makes your code depending on the data in your DB (contents of table roles) which is a bad idea.

rails how to save a model with has_many through association

I am having models like
// Contains the details of Parties (Users)
class Party < ActiveRecord::Base
has_many :party_races
has_many :races, :through=>:party_races
end
// Contains the party_id and race_id mappings
class PartyRace < ActiveRecord::Base
belongs_to :party
belongs_to :race
end
// Contains list of races like Asian,American,etc..
class Race < ActiveRecord::Base
has_many :party_races
has_many :parties, :through => :party_races
end
Now, lets say I'm creating an instance of Party
party_instance = Party.new
How am I supposed to add multiple Races to party_instance and save to database ?
You could use nested attributes to make one form that allows children. There are many examples on this site. Read up on the following first:
Accepts nested attributes
Fields for
A railscast about your use case
You also can create new PartyRace for each Races that you can add:
def addRace( party_instance, new_race )
party_race = PartyRace.new( party: party_instance, race: new_race )
party_race.save
end

Resources