Polymorphic has_many through Controllers: Antipattern? - ruby-on-rails

I'm tempted to say yes.
A contrived example, using has_many :through and polymorphs:
class Person < ActiveRecord::Base
has_many :clubs, :through => :memberships
has_many :gyms, :through => :memberships
end
class Membership < ActiveRecord::Base
belongs_to :member, :polymorphic => true
end
class Club < ActiveRecord::Base
has_many :people, :through => :memberships
has_many :memberships, :as => :member
end
etc.
Leaving aside, for the moment, the question of whether a Gym is a Club, or any other design flaws.
To add a User to a Club, it's tempting to be RESTful and POST a person_id and a club_id to MembersController, like so:
form_for club_members_path(#club, :person_id => person.id) ...
In this scenario, when we decide to do:
form_for gym_members_path(#gym, :person_id => person.id) ...
We would need to make MembersController decide whether the parent resource is a Club or a Gym, and act accordingly. One non-DRY solution:
class MembersController < ApplicationController
before_filter :find_parent
...
private
def find_parent
#parent = Gym.find(params[:gym_id]) if params[:gym_id]
#parent = Club.find(params[:club_id]) if params[:club_id]
end
end
Shockingly awful if you do it more than once.
Also, it's predicated on the concept that joining a Club and joining a Gym are roughly the same. Or at least, Gym#add_member and Club#add_member will behave in a more or less parallel manner. But we have to assume that Gyms and Clubs might have different reasons for rejecting an application for membership. MembersController would need to handle flash messages and redirects for two or more error states.
There are solutions in the wild. James Golick's awesome ResourceController has a way of dealing with parent_type, parent_object, etc. Revolution On Rails has a nice solution for DRYing up multiple polymorphic controllers by adding some methods to ApplicationController. And of course, ActionController has #polymorhpic_url for simpler cases like Blog#posts and Article#posts, etc.
All this leaves me wondering, is it really worth putting all that pressure on MembersController at all? Polymorphism is handled pretty well in Rails, but my feeling is that using conditionals (if/unless/case) is a clear indication that you don't know what type you're dealing with. Metaprogramming helps, but only when the types have similar behavior. Both seem to point to the need for a design review.
I'd love to hear your thoughts on this. Is it better to be DRY in this scenario, or to know exactly what parent type you have? Am I being neurotic here?

Related

Rails association with almost all other models

I'm looking for some suggestions on how to deal with "Regions" in my system.
Almost all other models in the system (news, events, projects, and others) need to have a region that they can be sorted on.
So far, I've considered a Region model with has_many :through on a RegionLink table. I've never had a model joined to so many others and wonder if this route has any negatives.
I've also considered using the acts_as_taggable_on gem and just tag regions to models. This seems ok but I'll have to write more cleanup type code to handle the customer renaming or removing a region.
Whatever I choose I need to handle renaming and, more importantly, deleting regions. If a region gets deleted I will probably just give the user a choice on another region to replace the association.
Any advice on handling this is greatly appreciated.
If each News, Event, etc. will belong to only 1 Region, tags don't seem the most natural fit IMO. This leaves you with 2 options:
Add a region_id field to each model
This is simplest, but has the drawback that you will not be able to look at all the "regioned" items at once - you'll have to query the news, events, etc. tables separately (or use a UNION, which ActiveRecord doesn't support).
Use RegionLink model with polymorphic associations
This is only slightly more complicated, and is in fact similar to how acts_as_taggable_on works. Look at the Rails docs on *belongs_to* for a fuller description of polymorphic relationships if you are unfamiliar
class Region < ActiveRecord::Base
has_many :region_links
has_many :things, :through => :region_links
end
# This table with have region_id, thing_id and thing_type
class RegionLink < ActiveRecord::Base
belongs_to :region
belongs_to :thing, :polymorphic => true
end
class Event < ActiveRecord::Base
has_one :region_link, :as => :thing
has_one :region, :through => :region_link
end
# Get all "things" (Events, Projects, etc.) from Region #1
things = Region.find(1).things
Renaming is quite simple - just rename the Region. Deleting/reassigning regions is also simple - just delete the RegionLink record, or replace it's region_id.
If you find yourself duplicating a lot of region-related code in your Event, etc. models, you may want to put it into a module in lib or app/models:
module Regioned
def self.inluded(base)
base.class_eval do
has_one :region_link, :as => :thing
has_one :region, :through => :region_link
...
end
end
end
class Event < ActiveRecord::Base
include Regioned
end
class Project < ActiveRecord::Base
include Regioned
end
Checkout the cast about polymorphic associations. They did change a bit in rails 3 though: http://railscasts.com/episodes/154-polymorphic-association?view=asciicast

Modify the behavior of has_many or use scope?

I have a class that looks something like this:
class User < ActiveRecord:Base
has_many :users_companies
has_many :companies, :through => :users_companies
end
For plain users, I'd like user.companies to refer to the standard association method, but when a user is an admin, I want User.all (i.e., admins have access to all companies). The simplest way I can think of to implement this (and what I've always done in the past) is use a scope on the Company class, such as:
scope :accessible_by, lambda { |user| ... }
The only problem is that this just doesn't feel right. Instead of writing a controller action that includes:
#companies = Company.accessible_by(current_user)
I'd feel more comfortable writing
#companies = current_user.companies
Is there a good way to override the User#companies method to accommodate this kind of behavior? Or, should I be happy with using a scope on Company?
I'm wrestling with a similar problem. The only acceptable solution I can devise is an association extension, which overrides the query for admin users and passes normal users' queries, unmolested.
# this works for me in rails 3.1
class User < ActiveRecord:Base
has_many :users_companies
has_many :companies, :through => :users_companies do
def visible
if proxy_association.owner.admin?
UsersCompany.scoped
else
self
end
end
end
end
User.where(:admin => true).first.companies.visible == UsersCompany.all
I'm fairly new to Rails, but this is an interesting question so I figured I'd toss in my two cents. It seems that you should be able to extend your association in User with a companies method that checks self.is_admin? (or similar) and returns what you need. See http://apidock.com/rails/ActiveRecord/Associations/ClassMethods/has_many#461-User-a-block-to-extend-your-associations
Nice question. I was wondering if something like the following is an option you would consider
class User < ActiveRecord:Base
has_many :users_companies
has_many :companies, :through => :users_companies
def viewable_companies
admin? ? Company.all : self.companies
end
end
I know the naming is horrible but, you know, naming things is serious stuff :)

Rails ActiveRecord Double Associations

I have the following two models, User..
class User < ActiveRecord::Base
has_and_belongs_to_many :sites
end
.. and Site:
class Site< ActiveRecord::Base
has_and_belongs_to_many :users
end
Up to this point its fine. It works and it's pretty simple.
Now I want to introduce "primary user" to the Site. I add "primary_user_id" to the Site, and trying to add a second association:
class Site< ActiveRecord::Base
has_and_belongs_to_many :user
# my new association that doesn't work...
has_one :primary_user, :class_name => "User", :conditions => ['id = ?', '{self.primary_user_id}']
end
It doesn't like it... Now I know that I can fake this by just adding a method "primary_user" to the site and this will work, but my question is whether it is possible to user ActiveRecord associations and how?
has_and_belongs_to_many is tricky and most people have moved away from it and use has_many through => model.
btw - 'Up to this point its fine. It works and it's pretty simple.' is how all things start off. How they perform when you 'really' start to use them is what counts and for that reason you'll probably find has_many through easier to work with.
These links will help:
http://paulbarry.com/articles/2007/10/24/has_many-through-checkboxes
http://thoughtsincomputation.com/posts/checkboxes-with-has_many-through
http://my.opera.com/durrantm/blog/2011/07/24/rails-simple-form-with-has-many-through-hmt-relationship
https://github.com/romanvbabenko/nested_has_many_through (nesting gem).

Rails query using HMTH and multiple join models

I am using Rails 3 and wanted to get the classes a student has access to based upon the model below
class Student
has_many :students_levels
has_many :levels, :through => :students_levels
end
class Class
has_many :classes_levels
has_many :levels, :through => :classes_levels
end
class Level
has_many :students_levels
has_many :classes_levels
end
class StudentsLevel
belongs_to :students
belongs_to :levels
end
class ClassesLevel
belongs_to :classes
belongs_to :levels
end
I came up with the query below but didn't think it seemed like the best Rails way to do things and wanted to get additional suggestions. Thx
Class.where(:id => (ClassesLevel.where(:level_id => Student.find(1).levels)))
I want to add this as an instance method to Student and was thinking there would be a better way doing something with has many through.
I quite did not understand the whole logic behind your class structure. Why you are not connecting students directly into a class? And how a class can have many levels. I mean if you have Math1 and Math2, those are different classes, right? Or do you have Math1,2,3?
Well, anyway, here's the solution if you want to use current assosiations, I hope it suites your needs:
Class Student
...
def available_classes
Class.find(:all,
:include => {:levels => {:students_levels => :student}},
:conditions => ["students.id = ?", self.id])
end
And sorry, this is still in Rails 2.x format...

RESTfully destroy polymorphic association in Rails?

How do I destroy the association itself and leave the objects being associated alone, while keeping this RESTful?
Specifically, I have these models:
class Event < ActiveRecord::Base
has_many :model_surveys, :as => :surveyable, :dependent => :destroy, :include => :survey
has_many :surveys, :through => :model_surveys
end
class ModelSurvey < ActiveRecord::Base
belongs_to :survey
belongs_to :surveyable, :polymorphic => true
end
class Survey < ActiveRecord::Base
has_many :model_surveys
end
That's saying that the Event is :surveyable (ModelSurvey belongs_to Event). My question is, without having to create a ModelSurveysController, how do I destroy the ModelSurvey, while leaving the Event and Survey alone?
Something with map.resources :events, :has_many => :model_surveys? I'm not quite sure what to do in this situation. What needs to happen with the routes, and what needs to happen in the controller? I'm hoping the url could look something like this:
/events/:title/model_surveys/:id
Thanks for your help,
Lance
In Rails 2.3 you have accepts_nested_attributes_for which would let you pass an array of ModelSurveys to the event in question. If you allow destroy through the nested attributes declaration, you'll be able to pass event[model_surveys][1][_destroy]=1 and the association will be removed. Check out the api docs.
Resources domain != model domain
The domain of the controller is not the same as that of the models. It's perfectly fine to update multiple models by changing the state of a resource.
In your case that means doing a PUT or POST to either the Event or the Survey which contains a list of ids for the other. The model for one will update the association.
PUT or POST
Some people (but not Roy Fielding) believe that you should use a PUT to update the resource and provide all of the state again, others feel that a POST with the partial state (ala PATCH) is sufficient.

Resources