views:

171

answers:

2

I have the following models:

class User < ActiveRecord::Base
  has_many :subscriptions
end

class Subscription < ActiveRecord::Base
  belongs_to :user
  belongs_to :queue
end

class Queue < ActiveRecord::Base
  has_many :subscriptions
end

I want to have some meta-data in the Subscription class and allow users to maintain the details of each of their subscriptions with each subscriptions meta-data. Queues produce messages, and these will be sent to users who have Subscriptions to the Queue.

As I see it the resource I want to have is a list of subscriptions, ie the user will fill in a form that has all the Queues they can subscribe to and set some metadata for each one. How can I create a RESTful Rails resource to achieve this? Have I designed my Subscription class wrong?

I presently have this in my routes.rb:

map.resources :users do |user|
  user.resources :subscriptions
end

But this makes each subscription a resource and not the list of subscriptions a single resource.

Thanks.

+2  A: 

This can be done quite easily using accepts_nested_attributes_for and fields_for:

First in the User model you do the following:

class User < ActiveRecord::Base
  has_many :subscriptions
  accepts_nested_attributes_for :subscriptions, :reject_if => proc { |attributes| attributes['queue_id'].to_i.zero? }

  # if you hit scaling issues, optimized the following two methods
  # at the moment this code is suffering from the N+1 problem
  def subscription_for(queue)
    subscriptions.find_or_initialize_by_queue_id queue.id
  end

  def subscribed_to?(queue)
    subscriptions.find_by_queue_id queue.id
  end

end

That will allow you to create and update child records using the subscriptions_attributes setter. For more details on the possibilities see accepts_nested_attributes_for

Now you need to set up the routes and controller to do the following:

map.resources :users do |user|
  user.resource :subscriptions  # notice the singular resource
end

class SubscriptionsController < ActionController::Base

  def edit
    @user = User.find params[:user_id]
  end

  def update
    @user = User.find params[:user_id]
    if @user.update_attributes(params[:user])
      flash[:notice] = "updated subscriptions"
      redirect_to account_path
    else
      render :action => "edit"
    end
  end

end

So far this is bog standard, the magic happens in the views and how you set up the params: app/views/subscriptions/edit.html.erb

<% form_for @user, :url => user_subscription_path(@user), :method => :put do |f| %>
  <% for queue in @queues %>
    <% f.fields_for "subscriptions[]", @user.subscription_for(queue) do |sf| %>
      <div>
        <%= sf.check_box :queue_id, :value => queue.id, :checked => @user.subscribed_to?(queue) %>
        <%= queue.name %>
        <%= sf.text_field :random_other_data %>
      </div>
    <% end %>
  <% end %>
<% end %>
derfred
Thanks!I have made the changes you suggested with some tweaks and have the basics working. One problem is that I am getting rows in subscriptions with 0 as the queue_id. This is when I untick the checkbox. Is it possible to have these rows deleted when not checked?
Greg
try the following line in the user model:accepts_nested_attributes_for :subscriptions, :reject_if => proc { |attributes| attributes['queue_id'].to_i.zero? }
derfred
A: 

I found this tutorial very useful, as I was trying to relate Users to Users via a Follows join table: http://railstutorial.org/chapters/following-users

NatchiQ