views:

348

answers:

4

I have two scaffold-generated Models, student and class. They have a many-to-many relationship implemented with has_and_belongs_to_many.
I'd like to be able to change which classes a student is in as well as which students are taking each class. That is, I want to modify a student's classes variable (adding and removing items from it) and vice versa.
How do I do this RESTfully?
If I'm removing a class from a student's classes list, then it seems like I want to call update on my students_controller. If this is the case, then what should I pass in as a parameter to modify the classes variable? Another collection of classes (with the proper class removed)?
My other thought is just to call some action in the students_controller (say, remove_class) and pass in the ID of the class to be removed. This seems sensical but not RESTful.

What's the best way to do this?

A: 

If this is really REST, then you'd just change the class and the student at the same time, either adding the relationship to both or removing it from both. After checking that the uploaded model was self-consistent, you would then compare it against the previous one and make whatever changes are necessary. Whether this change is class-driven or student-driven becomes an implementation detail.

Two more options:

You could make the REST state information less redundant, by letting either the class or student own the relationship, but that may not fit your needs.

You could also keep the relationship information on both sides, but decide that one is canonical. So, for example, if having a student is owned by the class, not the student, then any change to the student's belongs_to is ignored.

I prefer the first option, but I feel that these should be at least considered.

Steven Sudit
A: 

I'm not sure you want to do both of them RESTfully. You'd probably get more mileage by creating a cascade delete relationship between both.

Here's an SO discussion on Cascade Deleting on Rails.

Robert Elwell
A: 

you could have two nested resources?

dependent on whether you have a params[:class_id] (please don't say you called your model Class!) or whether you had a params[:student_id] you could tell which is the object as to which the operation is being called on (see attached creation sample on one side):

class SchoolClassController < ApplicationController

before_filter :get_school_class, :except => [:new, :create, :index]
before_filter :get_student

def create
  get_school_class(false)
  get_student(false)
  if @school_class
    if @student
      (@school_class.students << @student)
    else
      redirect_to "/" and return
    end
  else
    @school_class = SchoolClass.new(params[:school_class])
  end
  if @school_class.save
    redirect_loc = @student ? student_school_class_path(@student, @school_class) : school_class_path(@student)
    redirect_to redirect_loc and return
  else
    redirect_to "/"
  end

end

private

def get_school_class(will_redirect=true)
  begin
    @school_class = SchoolClass.find(params[:id])
  rescue AR::RNF
    (redirect_to "/" and return false) if will_redirect
  end
end

def get_student(will_redirect=true)
  if params[:student_id]
    begin
      @student = Student.find(params[:student_id])
    rescue AR:RNF
      (redirect_to "/" and return false) if will_redirect
    end
  end
end

end

Excuse any errors! Should really be used as a theoretical example

Omar Qureshi
+2  A: 

The key to resolving this is to correctly identify the resource you are modifying. In this case, the resource you are modifying is the relationship between the class and the student, which I will refer to as an Enrollment.

It has become customary in Rails to use has_many :through preferentially to has_and_belongs_to_many. You may want to change your domain logic to fit the custom, but you can also buck the trend, if you are truly certain that no metadata needs to be stored about the relationship.

One of the key ideas for REST is that RESTful resources do not need to map to models. You should create an EnrollmentsController and add a line to config/routes.rb:

map.resources :enrollments

Then you can create and delete your relationships like so:

class EnrollmentsController < ApplicationController
    def create
       @student = Student.find(params[:student_id])
       @course = Course.find(params[:course_id])
       @student.courses << @course
       if @student.save
         #do happy path stuff
       else
         #show errors
       end
    end

    def destroy
       @student = Student.find(params[:student_id])
       @course = @student.courses.find(params[:course_id])
       @student.courses.delete( @course )
    end
end

And you can make buttons for those actions like so:

<%= button_to "Enroll", enrollments_path(:student_id => current_student.id, :course_id => @course.id ), :method => :post %>
<%= button_to "Withdraw", enrollment_path(1, :student_id => current_student.id, :course_id => @course.id ), :method => :delete %>

The 1 on the line above acts as a placeholder where the :enrollment_id should go and is a tiny bit of syntactic vinegar to remind you that you are bucking the will of the Rails framework.

austinfromboston
Thanks, that worked really well with two minor changes: 1. calling @student.courses.destroy( params[:course_id] ) gave me an ActiveRecord::AssociationTypeMismatch since it was looking for course and got a String. I fixed this by first finding the right course ( @student.courses.find(params[:course_id]) ) and then removing that. 2. I don't want to destroy the course, just remove the association so I used a delete call instead of destroy
Sam P
Glad that worked. I've updated my example to reflect your changes.
austinfromboston