Rails, self-joined table, proper way of creation - ruby-on-rails

For my web application I need to implement a supervisor/student relationship. I need to join my "Person" table with itself through the "Supervision" table.
class Person < ActiveRecord::Base
has_many :supervised, :class_name => 'Supervision', :foreign_key => 'supervisor_id'
has_many :supervisors, :class_name => 'Supervision', :foreign_key => 'supervised_id'
end
class Supervision < ActiveRecord::Base
belongs_to :supervised, :class_name => 'Person'
belongs_to :supervisor, :class_name => 'Person'
end
Now I need help regarding the controller. I'm not sure if I need two controllers, one for supervised and one for supervisors, or just one "Supervision" controller.
Both the student and supervisor must be able to create a "Supervision". I'm just not sure how to let the controller know whether the current user needs to be the supervisor or the student. Any thoughts?

You could create two controllers, but that would not be DRY, so it is probably best avoided. You can either set up your routes so the URLs for Prof/Student appear to be different, but actually map to the same controller.
How many students a prof has:
the_prof = Person.find( *my record number* )
the_prof.supervised.count
Who they are is that same thing, so show their names
the_prof.supervised.each do |student|
puts student.name
end
How to determine who is a professor or not? I would add a boolean flag to the people table: is_prof
My initial thought was the way to determine if a person was a student is they have no supervised. If a professor, they have no supervisor, but that breaks down if a Professor gets rid of all his students or the Student gets rid of all his Professors. Suddenly, we're in the land of undefined, which is BAD.
The flag also makes it easy to segregate all the professors and student, so you can do
profs = Person.find_by_is_prof( true )
studs = Person.find_by_is_prof( false )
(make sure to index that field in your database)

I'm guessing you'll end up making at least two controllers: one for people acting as supervisors, ie: one to manage one's subordinates; and one to manage one's own person record. You may even have another which manages a person's sign up and allows them to select their supervisor, as part of a kind of a wizard-style step.
Try mocking out how you want the application to behave first by sketching out some wireframes. Those will help you figure out which resources need to be changed from where.
If you find, for instance, that you need a list of one's subordinates, then that's probably a SubordinatesController#index page. Adding a subordinate would probably be a #new / #create pair in that controller.
Controllers are really about figuring out how the UI will respond to different user actions. Setting /my/ supervisor, and noting that I'm /someone else/'s supervisor are probably to very different things at the UI level. Just because they happen to reside in the same table doesn't mean that the UI has to reflect that symmetry.
It's strange that one would be able to change their own supervisor list. I think that's where the weirdness of your question arises.
Perhaps that's actually a side-effect of changing some other membership, like moving to a different group in the organization, in which case the reassignment would be part of its own controller.

Related

optimizing page load with has_many and scope

I'm an experienced programmer who is relatively new to ruby/rails and databases. I have created a large website for signing up for courses. I thought I was being clever creating categories of course signups using scope and has_many. Logically, it encapsulates the information well, but my pages are now loading super-slow, and trying to eager load is confusing me.
I have models for Course, Person, CourseRole (student, teacher, etc.), and CourseSignup which includes one of each (Course, Person, CourseRole). It all works smoothly. Recently, I set up scopes in CourseRole to define the categories of signups (I had been hardcoding the role name, and wanted to get away from that). I then set up has_many relationships in Course for each of the categories. So, Course inherently has_many course_signups, and has my categories
has_many :student_signups, -> { CourseSignup.student }, class_name: 'CourseSignup', foreign_key: :course_id
has_many :teacher_signups, -> { CourseSignup.teacher }, class_name: 'CourseSignup', foreign_key: :course_id
etc. (I have 6 categories). I have a page that lists all courses and all of the signups for each course. Like:
Dodge Ball:
info about course
Students:
names of students
Teachers:
names of teachers
etc.
This page loads incredibly slowly. I was trying to add includes statements to the query (based on recommendations from the Bullet gem), but it actually makes it slower. This leads me to think I'm making this more complicated than I should, but I don't know enough to have a clue how to fix it. I imagine I should restructure my models. But I like the abstraction of the scopes/has_many
The page is generated by looping through rendering a partial which shows one course.
#courses = #cuco_session.assigned_courses.includes(:period).order('periods.start_time')
works but is very slow.
#courses = #cuco_session.assigned_courses
.includes(course_signups: [:person, :course_role])
.includes([:courses_rooms, :rooms])
.includes([:helper_signups, :student_signups, :volunteer_signups, :waiting_list_signups, :person_in_room_signups])
.includes(:period).order('periods.start_time')
Also works but is even slower.
We need more information about what is actually happening in the view to really answer why your page is slow.
Also a snippet of your log showing the actual SQL would be necessary.
You mention being new to using databases online with Rails. It's hard to give advice without more of a look into the structure of some of these models, but have you considered replacing CourseSignup with just a join table between Course and Person? It seems like that's what you're getting at here but it's hard to tell.
*has_many :student_signups, -> { CourseSignup.student }, class_name: 'CourseSignup', foreign_key: :course_id
has_many :teacher_signups, -> { CourseSignup.teacher }, class_name: 'CourseSignup', foreign_key: :course_id*
above code can be refactored using Single table inheritance
# app/models/person.rb
class Person < ApplicationRecord
#your code
end
# app/models/teacher.rb
class Teacher < Person
#your code
end
# app/models/student.rb
class Student < Person
#your code
end
For Course Role you can have a separate master table of Role and put references of Person table and Role table in Course Role table
I hope above solution put some light on your question.

Rails abstract class has_many

As the title suggests, this isn't going to make any sense. Imagine the scenario:
I have the following models: Game, GameType, and Champion. I would like only games of a certain GameType (like MOBA) to have a has_many relationship to the Champion model; where others (like FPS, etc.) would not.
My first inclination was to make a GameTypeMoba abstract class, where all classes that inherit from it could have some of its properties (such as having champions). However, I know this doesn't make sense since a class that is not tied to a table can't have table relationships. Further, it just seems like a shitty, WET (opposite of DRY) approach if I could somehow hack it together.
I hope someone has a simple solution that doesn't involve messy app logic. Although I'd also accept "retard, go to bed" at this point as well.
Checking out the Rails Guides "has_many" association reference (http://guides.rubyonrails.org/association_basics.html#has_many-association-reference), you may be able to use the condition option on the association declaration. The example provided in the documentation:
class Customer < ActiveRecord::Base
has_many :confirmed_orders, :class_name => "Order",
:conditions => "confirmed = 1"
end
In your situation, I assume you would want to use the :class_name of "GameType" with :conditions => "MOBA = ".
Mischa is right, in this case it doesn't seem like there's anything better/cleaner that can be done. And having an unneeded relation for a subset of records isn't really a big deal.

How to decide which action to use

I'm very new to web-development (I feel like all my posts lately have started that way) and becoming, with time, less new to rails. I'm at a point where I can do a sizeable amount of the things required for my job but there's one nagging problem I keep running into:
How do I decide if which action I should use for a given task? index, show, new, edit, create, update or destroy?
destroy is pretty obvious and I can loosely divide the rest into two buckets with index/show in one and new/edit/create in the other. But how do I decide which one to use or if I should build one of my own?
Some general guidelines or links to further reading would be very beneficial for me.
Here is how I think of these 7 RESTful Controller actions. Take, for example, a Person resource. The corresponding PeopleController would contain the following actions:
index: List a set of people (maybe with some optional conditions).
show: Load a single, previously created Person with the intention of viewing. The corresponding View is usually "read-only."
new: Setup or build an new instance of a Person. It hasn't been saved yet, just setup. The corresponding View is usually some type of form where the user can enter attribute values for this new Person. When this form is submitted, Rails sends it to the "create" action.
create: Save the Person that was setup using the "new" action.
edit: Retrieve a previously created Person with the intention of changing its attributes. The changes have not been made or submitted yet. The corresponding View is usually a form that Rails will submit to the "update" action.
update: Save the changes made when editing a previously created Person.
destroy: Well, as you guessed, destroy or delete a previously created Person.
Of course there is some debate as to whether these 7 actions are sufficient for all controllers, but in my experience they tend to do the job with few exceptions. Adding other actions is usually a sign of needing an additional type of resource.
For example, say you have an HR application full of Person resources you are just dying to hire. In order to accomplish this, you may be tempted to create a "hire" action (i.e., /people/456/hire). However, a more RESTful approach would instead consider this the "creation" of an Employment resource. Something like the following:
class Person < ActiveRecord::Base
has_many :employments
has_many :employers, :class_name => 'Company', :through => :employments, :source => :company
end
class Employement < ActiveRecord::Base
belongs_to :person
belongs_to :company
end
class Company < ActiveRecord::Base
has_many :employments
has_many :employees, :class_name => 'Person', :through => :employments, :source => :person
end
The EmploymentsController's create action would then be used.
Okay, this is getting long. Don't be afraid to setup a lot of different resources (and you probably won't use all 7 Controller actions for each of these). It pays off in the long run and helps you stick to these 7 basic RESTful actions.
You can name your actions whatever you want. Generally, by Rails convention, index is the default one, show shows one item, list shows many, new and edit start editing a new or old item, and create and update will save them, respectively. destroy will kill an item, as you guessed. But all these are just conventions: you can name your action yellowtail if that's what you want to do.

Forem gem: how to link a forum to other models

I have groups (Group model) in my app, which represent groups of people.
I want each group to have its own forum.
Should I just have the forum id in the groups table? It doesn't feel right. If I did it myself, the forum would have a polymorphic association to a "forumable" element (groups in this case, but I have other models that would need a forum).
Any opinions on what I should do? Modify the gem to fit my needs, or just have the forum_id in my models that need a forum? Or another solution maybe?
I'm the guy who started Forem (its the volunteers who did most of the hard work, though!), I think I can answer this question.
If you want only certain groups to have access to one and only one forum then you can put the forum_id field on the groups table and do it that way. What you can do then is override the can_read_forem_forum? method in your User model to perform a permission check for that user:
def can_read_forem_forum?(forum)
groups.where(:forum_id => forum.id).any?
end
This is used in Forem's ability model to determine whether or not a person can access a forum. What this method is going to do is that it will only return groups for that user that have link that specific forum. If there are any, then it's known that the user can access that forum.
Now if you're going the other route where a group may have access to many forums, well then you'd define a joins table between groups and forem_forums (called forum_groups) and define it as an association in your Group model like this:
has_many :forum_groups
has_many :forums, :through => :forum_groups, :class_name => "Forem::Forum"
You would need to also define a new model inside your application for this forum_groups association, it would be called ForumGroup and go a little like this:
class ForumGroup < ActiveRecord::Base
belongs_to :forum, :class_name => "Forem::Forum"
belongs_to :group
end
We're doing it this way so you have an easy way to manage the associations between forums and groups. If you did has_and_belongs_to_many, it generally only provides a gigantic pain in the ass when you want to delete one specific record from that join table.
Now, with that all nicely set up, the method you want to define in your User model is this one:
def can_read_forem_forum?(forum)
groups.joins(:forums).where("forem_forums.id = ?", forum.id).any?
end
Same thing, except this time we find all the groups that are linked to a specific forum through that association we set up earlier. This will do an INNER JOIN on the forum_groups table, and then another on the forem_forums table, getting the data required.
I hope this helps you, and thanks for using Forem!

has_many :through object inhertiance

I am trying to make an application wherein Users have many Items, and each Item they have through Possession is an entity in its own right. The idea behind this is if I have a MacBook item, eg, and a user adds it to their inventory, they may apply attributes (photos, comments, tags, etc) to it without directly affecting them Item itself, only their Possession.
The Item will in turn aggregate attributes from its corresponding Possessions (if you were to go to /item/MacBook, rather than /user/101/possession/5). I have the following models setup (ignoring attributes like photos for now).
class User
has_many :possessions
has_many :items, :through => :possessions
end
class Item
has_many :possessions
has_many :users, :through => possessions
end
class Possession
belongs_to :user
belongs_to :item
end
My first question is, am I doing this right at all. Is has_many :through the right tool here?
If so, how would I deal with class inheritance here? I might not be stating this right, but what I mean is, if I were to do something like
#possession = Possession.find(params[:id])
#photos = #possession.photos.all
and there were no photos available, how could it fall back to the corresponding Item and search for photos belonging to it?
Your initial data structure seems appropriate.
As for the second part, with the "fall back" to a corresponding item, I don't think there would be a direct Active Record way of doing this. This behavior seems pretty specific, and may be confusing to future developers working on your app unless you have a clear method for this.
You could create a method inside Possession like:
def photos_with_fallback
return self.photos if self.photos.size > 0
self.item.photos
end
There is a huge consequence to doing this. If you have a method like this, you won't be able to do any write activities down the wrode like #photos.build or #photos.create because you won't know where you're putting them. They could be linked to the Item or the Posession.
I think you're better of pushing the conditional logic out to your controller and checking for photos on the Posession first and then on the Item.
#In the controller
#photos = #posession.photos
#photos = #posession.item.photos if #photos.size == 0
This will be more clear when you go to maintain your code later, and it will allow you to make other decisions down the road.

Resources