Assign user model to one of two models? - ruby-on-rails

I have a user model that I want to assign to be teacher or student( teacher and student are two separated model ) because if the user signup he would have different registration fields depending on if he is teacher or student. User can be a teacher or student, not both.
I have tried, but don't think that this is the best way to do it. Any help?
class User < AR
has_secure_password
has_one :teacher, class_name: "teacher", foreign_key: "teacher_id", conditions: { role: 'teacher' }
has_one :student, class_name: "student", foreign_key: "student_id", conditions: { role: 'student' }
enum role: [:teacher, :student]
end
class Teacher < AR
belongs_to :user, class_name: "user", foreign_key: "user_id"
end
class Student < AR
belongs_to :user, class_name: "user", foreign_key: "user_id"
end

This is how you can implement the STI for your case
class User < AR
has_secure_password
# Make all forms with User data send params with ':user' as a param key
# instead of ':user_teacher'/':user_student'
def self.model_name
ActiveModel::Name.new(self, nil, 'User')
end
end
class Teacher < User
# custom methods
end
class Student < User
# custom methods
end
This way, you can have your form with form_for #user do # ....
One caveat, it is all placed in the Single table in the DB (hence the name Single Table Inheritance), and that means a lot of NULL values for the unrelated fields (say Teacher has a teacher_identification_number, and a user has student_identification_number which are different in size or they require different validation) for all the Students that attribute teacher_identification_number would be NULL, and vice-versa.
If the fields are much different between the two models, then you can analyze your data and put it in the different table to which only the Teacher/Student would have access to, that is called a Database Normalization (say Teacher has_many ClassInfo's or has_one JobInfo; or Teacher has_one TeacherProfile, and Student has_one StudentProfile or whatever).
It all really depends on how you model your DB.
References:
- Blog Post - Medium - STI
- Video Link - Drifting Ruby - STI
- Video Link - # RailsCasts - STI
- Blog Post - StudyTonight - DB Normalization

Related

accepts_nested_attributes_for is contradicting the relational database logic in Rails

Ruby on Rails model logic conflicts with the relational database logic.
In Ruby on Rails, the model possessing the belongs_to, will have the foreign key in the database. So, the model:
class Student < ApplicationRecord
belongs_to :university
end
class University < ApplicationRecord
has_many :university
end
Will have the migration file:
class CreateStudents < ActiveRecord::Migration[6.0]
t.string :name
t.references :university, foreign_key: true
end
class CreateUniversities < ActiveRecord::Migration[6.0]
t.string :name
end
And, logically, the student will have the University foreign_key in the database, like so:
Student
-id int (PK)
-name varchar
-University_id int (FK)
University
-id int (PK)
-name varchar
So good so far
The problem arises when I want accepts_nested_attributes_for in the student model for referencing a University in the _form.html.erb. In order to use it, the host model, in this case Student, must have a has_one instead of a belongs_to for the referencing table. So it would become:
class Student < ApplicationRecord
has_one :university
accepts_nested_attributes_for :university
end
Notice how, in order to use accepts_nested_attributes_for for referencing the university, the Student model MUST replace its belongs_to :university for a has_one :university, and University would have a belongs_to :student, instead of a has_many :student.
So in the migration file, Student would lose its reference to University, and the latter would have a reference to Student instead, like so:
class CreateStudents < ActiveRecord::Migration[6.0]
t.string :name
end
class CreateUniversities < ActiveRecord::Migration[6.0]
t.string :name
t.references :student, foreign_key: true
end
And the database would forcefully be:
Student
-id int (PK)
-name varchar
University
-id int (PK)
-name varchar
-Student_id int (FK)
Which is wrong, because there should be a one to many relationship from University to Student, not the other way around.
So, is there a way to use accepts_nested_attributes_for, without messing up the relational database logic by having to forcefully inverse the relations in the models ?
accepts_nested_attributes_forNested attributes allow you to save attributes on associated records through the parent.
So you might be having a slight mix up about which one you want to be the parent. If you make the student the parent with nested attributes for the university you would not be referencing the university but essentially creating a new university record or updating an existing one with the nested attributes you used in your student form.
One-to-one
Consider a Member model that has one Avatar:
class Member < ActiveRecord::Base
has_one :avatar
accepts_nested_attributes_for :avatar
end
Enabling nested attributes on a one-to-one association allows you to create the member and avatar in one go:
params = { member: { name: 'Jack', avatar_attributes: { icon: 'smiling' } } }
member = Member.create(params[:member])
member.avatar.id # => 2
member.avatar.icon # => 'smiling'
So you would only be updating/creating new University records for each student you used those nested attributes on.
Here is for one-to-many:
One-to-many
Consider a member that has a number of posts:
class Member < ActiveRecord::Base
has_many :posts
accepts_nested_attributes_for :posts
end
You can now set or update attributes on the associated posts through an attribute hash for a member: include the key :posts_attributes with an array of hashes of post attributes as a value.
For each hash that does not have an id key a new record will be instantiated, unless the hash also contains a _destroy key that evaluates to true.
params = { member: {
name: 'joe', posts_attributes: [
{ title: 'Kari, the awesome Ruby documentation browser!' },
{ title: 'The egalitarian assumption of the modern citizen' },
{ title: '', _destroy: '1' } # this will be ignored
]
}}
member = Member.create(params[:member])
member.posts.length # => 2
member.posts.first.title # => 'Kari, the awesome Ruby documentation browser!'
member.posts.second.title # => 'The egalitarian assumption of the modern citizen'
You can also see that in the one to many example above. Are you looking to change/create a University record when you post a Student record?
If you are just looking for a reference you essentially have that in your first example and could just change your routes up a bit to have the student referenced by the university.
Then once you have the routes nested you can just do a little work to the form partial. Here is a guy doing just that
So maybe if I am way off here describe why when you create/update a Student you want to create/update a University. If that is what you want to do.
This would helps others with your context a little more and might help others understand your intent with the nested attributes.
Add your form partial for example and explain your goal.
EDIT:
As a punt you can maybe look at has and belongs to many association.
This tutorial talks about nested attributes with a many to many association. But since I'm not certain exactly what your after it may or may not help.
Another punt sounds like you want maybe a student's university record to change when you update them. So you would have the student belong to the university and the university to has many students but also has many university records and the student to has many/has one university record and the record to belong to both.
aka:
this
Then you could have the student have accepts nested attributes for the university record/s.
I see that I have REALLY made a blunder trying to explain my doubts (it is my first question, I have learned from my mistake).
The ultimate goal was to know:
Can I use accepts_nested_attributes_for when there is a belongs_to instead of a has_one or has_many ?
So, the answer is yes:
class Student < ApplicationRecord
belongs_to :university
accepts_nested_attributes_for :university
end
In the student controller, there should be a build_:
def new
#student = Student.new
#student.build_university
end
I REALLY, REALLY, REALLY mistyped my question, and I apologize for it.
Thanks for all your answers.

Having a many-to-many association in the same model rails

I have a model called User and for my model, a user can either be a Leader or a Member. In my user model i have this
class User < ActiveRecord::Base
attr_accessible :username, :type
end
I thought i could create a many-to-many association in the User model like this
class User < ActiveRecord::Base
attr_accessible :username, :type
has_and_belongs_to_many :users, :join_table => :team_members, :foreign_key => :team_leader_id
end
But i am not really sure how to go about it. So for example.
User 1 - type :leader
User 2 - type : member
User 3 - type: member.
I want to create a relationship that can show that User 1 is the leader of user 2 and user 3.
I am still a bit new to rails .
add leader_id to User
class User < ActiveRecord::Base
attr_accessible :username, :type
belongs_to :leader, class: User
has_many :members, class: User, foreign_key: :leader_id
end
use :
#user_1 = User.create(name: "Jhon")
#user_2 = User.create(name: "Tom", leader: #user_1)
#user_1.members
well, you have User table and user can be member or leader,
if you sure that there will not be any other roles
you can use boolean leader and can be true or false if false that mean this user is member
if you not sure if there may be any other roles
you can go with what you currently being used type column and can contain member or leader that is for the first part.
then you need leader to control many members then there is 2 possibilites:
Member can belongs to only 1 Leader then you will need to add a new column in user table called leader_id for example and in this case it will be
has_many :members, :class_name => "User", :foreign_key => :leader_id
Member can belongs to many leaders then you will need to create many to many relation and then will use a new table that contain leader_id and member_id and both should be refer to user table as a forigen keys.
And better than all and have this relation in User model and its only valid for Leader you can have 2 models that inherit from User and that is called STI Single Table Inheritance you can read more about it here:
class User < ActiveRecord::Base
# this type will be checked if Leader then its Leader model if Member then its Member model
self.inheritance_column = 'type'
end
class Member < User
end
class Leader < User
has_many :members, :class_name => "User", :foreign_key => :leader_id
end
this model is away better than all, and in this case let's say in your User model you have:
1 User type='Member'
2 User type='Leaeder'
if you say:
# will work
Member.find 1
User.find 1
# will fail as type is not Leader
Leader.find 1

Rails association in concerns

I am using concerns for my rails application. I've different kind of users so I have made a loggable.rb concern.
In my concern I have
included do
has_one :auth_info
end
because every of my user that will include the concern will have an association with auth_info table.
The problem is, what foreign keys I need to put in my auth_info table?
E.G
I've 3 kind of users:
customer
seller
visitor
If I had only customer, in my table scheme I would have put the field
id_customer
but in my case?
You can solve this with polymorphic associations (and drop the concern):
class AuthInfo < ActiveRecord::Base
belongs_to :loggable, polymorphic: true
end
class Customer < ActiveRecord::Base
has_one :auth_info, as: :loggable
end
class Seller < ActiveRecord::Base
has_one :auth_info, as: :loggable
end
class Visitor < ActiveRecord::Base
has_one :auth_info, as: :loggable
end
Now you can retrieve:
customer.auth_info # The related AuthInfo object
AuthInfo.first.loggable # Returns a Customer, Seller or Visitor
You can use rails g model AuthInfo loggable:references{polymorphic} to create the model, or you can create the migration for the two columns by hand. See the documentation for more details.
Since user has roles 'customer', 'seller', 'visitor'.
Add a column called role to Users table.
Add a column called user_id to auth_infos table.
class AuthInfo < ActiveRecord::Base
belongs_to :user
end
class User < ActiveRecord::Base
has_one :auth_info
end
you can do
user = User.first
user.auth_info
Now you a additional logic to your concerns.

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.

Resources