Complex user relationships in Rails/Devise app - ruby-on-rails-3

Rails app using Devise for user authentication. Started simple, now getting quite complex.
There are 3 kinds of user that will be logging in to the site: Admin, Teacher, Parent. I have a single Devise User model and User.role string for these. There is a Student model (which doesn't log in) and a Lesson model.
These are the relationships I need:
Lesson belongs to one Student
Student has many Lessons
Student has many Teachers
Student belongs to one Parent
Parent has many Students
Teacher has many Students
Basic functionality of the site: Teachers log in and make lessons for their students. Parents log in and see all lessons for their kids (students). Admins log in and can CRUD teachers, parents and students.
I have most of it working but got stumped when I tried to implement 'student has many teachers' and 'parent has many students'. My initial (incorrect) assumptions were that a student only has one teacher and that a parent just has one child at the school.
So it seems that using a User.role string won't suffice for these relationships.
Any advice or general concepts would be greatly appreciated.

Are you just looking for a join table, ie a has_and_belongs_to_many relation?
http://guides.rubyonrails.org/association_basics.html#the-has-and-belongs-to-many-association
As I see it:
class Student
belongs_to :parent
has_and_belongs_to_many :teachers
end
class Parent
has_many :students
end
class Teacher
has_and_belongs_to_many :students
end

There can be multiple ways of solving this, what i understand is that you have solved this much but you have made things bit complex.
you can simply have 1 model `User' and remaining things can be polymorphic.
You can move the common attributes in User and separate the remaining in their corresponding models.
It would be much better if had a look at code.
Thanks

I ended up creating a third model and using has_many :through => to link everything together. I wanted to keep a single Devise user model and use a .role for authorization.
The third model I created was Timeslot, which is essentially where a teacher and student interact. Ideally I would have called it class or session but they're both reserved words.
# timeslot.rb
class Timeslot < ActiveRecord::Base
belongs_to :user
belongs_to :student
end
# user.rb
class User < ActiveRecord::Base
ROLES = %w[admin teacher parent]
has_many :timeslots
has_many :students, through: :timeslots
end
# student.rb
class Student < ActiveRecord::Base
has_many :lessons
has_many :timeslots
has_many :users, through: :timeslots
end
This allows a user with a teacher role to have many students and vice-versa. The Student model has a parent_id column which stores the id of the user with role parent. That part is still a bit clunky but it works fine for now. I'm sure I'll have to refactor to something more elegant if the app has to scale.

Related

Advanced active record query, or a way to use will_paginate with an array

I need to use the will_paginate gem on a part of my project but as far as I can tell it only works directly on database objects. However I can't figure out this one ActiveRecord for the life of me.
I have three tables:
class Vote
belongs_to :movie
belongs_to :user
end
class User
has_many :movies
has_many :votes
end
class Movie
has_many :votes
belongs_to :user
end
The user has the ability to vote on numerous movies, and they can upload their own movie titles. I need to return the unique movie titles that the current user has voted on. It's also important to note that a user can vote on the same movie multiple times.
I can do it in straight ruby no problem :
current_user.votes.map{|vote| vote.movie}.uniq
But ActiveRecord is a mystery.
Thanks in advance for your help!
Assuming each user can only vote on a given movie one time.. then since you have the associations, I would just get the user, the users votes and join in movies.
current_user.votes.joins(:movie)
This will provide an active record relation which can use the will_paginate methods..
current_user.votes.joins(:movie).paginate(:page => params[:page])
Edit
Since in your case a user can vote a movie multiple times, it's easier to do the query in reverse, and join the votes to the user.movies and call uniq. Like so:
current_user.movies.joins(:votes).uniq

Preloading using includes on unrelated models

Suppose I have three models, set up something like this:
class Student < ActiveRecord::Base
has_many :tests
has_many :cars
end
class Car < ActiveRecord::Base
belongs_to :student
end
class Test < ActiveRecord::Base
belongs_to :student
end
I want to query all tests whose student does not have car. Preloaded.
I've tried the following:
Test.includes(:cars) # does not work because the two are not associated
Test.joins('inner join cars ON tests.student_id = cars.student_id') # works, but it doesn't preload the Cars model in my result
I'd prefer not to create a has_many :through relationship, because they really aren't related at all, but I'm not opposed to it if that's the best solution.
Thoughts?
Rails 4.1.5
PostgreSQL 9.3.4
ruby 2.1.2
A join across three tables is an inefficient way to do this. Rails might even be smart enough to realise this and split it into seperate db queries.
I would do it like this, which has two simple queries instead
student_ids_with_car = Car.select("student_id").distinct
#tests = Test.where("student_id not in (?)", student_ids_with_car)
You don't have to use has_many :through to associations and associations of those associations at the same time.
Test.includes(:student => :cars)
Will include student's and their cars (by default it will preload, you can force a joins based include by using eager_load instead of preload).

Ruby-newbie Active Record query / model association

Here is roughly what I have:
I have many companies that has many user through roles:
class Company < ActiveRecord::Base
has_many :roles, :dependent => :destroy
has_many :users, :through => :roles
end
and many users which can have many companies through roles:
class User < ActiveRecord::Base
has_many :roles, :dependent => :destroy
has_many :comapnies, :through => :roles
end
and many roles that bind the two together:
class Role < ActiveRecord::Base
belongs_to :user
belongs_to :company
end
The idea here is that a user can sign-in through a common email username and then have privileges assigned to each company. Role has a user_id and company_id keys along with a privilege column 'user_role' (i.e. guest, user, admin etc.).
Here is where being ruby-newbie fails me. When my user logs in I want to query which companies they have privileges on and switch that on the fly with a menu drop down. So they login and are active in 'Company A' and then they can switch to 'Company B'. When they switch the company_id is stored in a session variable (I already have their user_id stored with a functioning authentication system).
What is the best way to query this? I have a user_id and I want to retrieve a list of the company_id, company_name, 'user_role' etc. then use this to generate a pop-down list.
I have been playing with the console trying Role.Company.etc.etc but can't seem to figure this out. When I was doing this in PHP / MySQL before it would have been a complex join query. I am sure it's easier in Rails I jet can't seem to figure it out.
Any help would be appreciated.
Thanks,
Dan
You can get all of the companies a user has access to (for any role) via current_user.companies.uniq. The uniq part just ensures you don't have duplicate companies in the list (in the case a user has more than one role for a single company).
Thanks for everyones feedback. This got me looking at includes and joins again. I ended up looking at http://railscasts.com/episodes/181-include-vs-joins and the Rails API docs and cobbled together this:
Role.includes(:company).where(:user_id => session[:user_id])
I then loop through #role and I can pull role.role_name and role.company.name etc. Starting the query with the User.find(session[:user_id]) was complicating it.

Writing foreign key value into belongs_to model after create Rails 3

I have a basic problem with a has_one and belongs_to association. I have two models: StudentRegistration and User that look like the following:
class StudentRegistration < ActiveRecord::Base
belongs_to :user
end
and
class User < ActiveRecord::Base
has_one student_registration
end
The idea is that a student gets registered and then later a User account can be created. My problem is that I'm storing the foreign key user_id in the student_registration table and I only know this once the user record has been created. When the user account gets created I need to update the student_registration table adding the new user id.
Is it the case that I just have this the wrong way around or should Rails handle this automatically?
You have setup your associations the correct way, but I guess the implementation is wrong.
No problems, just go through this link and you'd be good to go.
Nested-model-form-part-1 - Railscasts

ActiveRecord Associations: Any gotchas if has_many WITHOUT corresponding belongs_to?

A phone has many messages.
An email address has many messages.
A message either belongs to a phone, email, or neither. The belongs_to association is optional.
The following associations seem to work fine for these relationships:
Phone model has_many :messages
Email model has_many :messages
Message model does NOT have belongs_to :phones, :email
Is this okay or is there some proper way to specify a "can_belong_to" relationship?
It is completely correct unidirectional relation. Using both is sometimes called "curcular dependency" by some purists and may cause problems when using validates_associated.
From the other side using only has_many :messages may be not enough when you want retrieve phone information from one message. Generally it is matter of convenience.
The model with the belongs_to associations holds the foreign keys (e.g. messages table would have phone_id and email_id columns).
The belongs_to association combined with has_many lets you easily access associated records:
phone.messages
message.phone
So without the belongs_to and FK columns, the has_many association isn't very useful.
It seems like in this case you may want a many-to-many relationship such as has_and_belongs_to_many as a message can have many recipients.