Topic: Two Similar, yet different Tasks...how to DRY?

I have an existing application at birdsite.org. This site lets people upload bird photos. Other users can comment on the photos. When a comment is made, all users subscribed to that image will receive an email notice.

I'm now adding a forum. I want the same subscription behavior to occur when someone adds a post to a forum topic. The only differences with the image subscriptions is that the forum subscription references a Forum Post ID instead of a Image ID, and the content of the generated email refers to the forum instead of an image.

How would you keep this DRY? I'm mainly interested in making the Ruby code DRY, but it might be nice to store subscription data in one table as long as it's not too contrived.

So far all I've done is make the email template flexible enough to say image or forum, and provide the proper hyperlink. But I've got two very similar subscription models and tables, and similar code for making the notifications.

This code is representative of what I'm doing, but not the exact code:

# image_subscription table contains: id, image_id, user_id
class ImageSubscription < ActiveRecord::Base
  belongs_to :user
  belongs_to :image
  validates_uniqueness_of :image_id, :scope => :user_id
end

# image_subscription table contains: id, forum_topic_id, user_id
class ForumSubscription < ActiveRecord::Base
  belongs_to :user
  belongs_to :forum_topic
  validates_uniqueness_of :forum_topic_id, :scope => :user_id
end

# My ImageComment class has an after_create method that subscribes
# the current user and sends notifications
  def after_create
    new_subscription = self.image.image_subscriptions.build()
    new_subscription.user_id = session[:user_id]
    new_subscription.save

    subscriptions = ImageSubscription.find_all_by_image_id(self.image.id,
      :conditions => ['user_id <> ?', session[:user_id]]) rescue nil

    if subscriptions
      subscriptions.each do | subscription |
        begin
          Mailer::deliver_subscription_message('image', self.image.title,
            self.image.id, self.id, subscription.user.email)
        rescue
          # note error
        end
      end
    end
  end

# My ForumPost class has an after_create that looks just like the above but
# refers to the ForumSubscription model and the Mailer::deliver line replaces
# all the references to 'image' with 'forum_topic'.


Suggestions are appreciated.

Re: Two Similar, yet different Tasks...how to DRY?

I think you could achieve that with a polymorphic belongs_to on a single Subscription model.

class Subscription < ActiveRecord::Base
  belongs_to :user
  belongs_to :subscribable, :polymorphic => true

  belongs_to :image, :class_name => 'Image',
             :foreign_key => 'subscribable_id'
  belongs_to :forum_topic, :class_name => 'ForumTopic',
             :foreign_key => 'subscribable_id'
end

class User < ActiveRecord::Base
  has_many :subscriptions
  has_many :subscribables, :through => :subscriptions

  has_many :image_subscriptions, :through => :subscriptions,
           :source => :image,
           :conditions => "subscriptions.subscribable_type = 'Image'"
  has_many :forum_topic_subscriptions, :through => :subscriptions,
           :source => :forum_topic,
           :conditions => "subscriptions.subscribable_type = 'ForumTopic'"
end

class ForumTopic < ActiveRecord::Base
  has_many :subscriptions, :as => :subscribable
  has_many :subscribers, :through => :subscriptions, :source => :user
end

class Image < ActiveRecord::Base
  has_many :subscriptions, :as => :subscribable
  has_many :subscribers, :through => :subscriptions, :source => :user
end


I think that's right, then the Subscription model migration would look something like:

create_table :subscriptions do |t|
  t.integer  :user_id, :subscribable_id
  t.string   :subscribable_type
  t.datetime :created_at
end

Then for assignment + fetching:

## subscribe to forum_topic
Subscription.create(:user => current_user, :subscribable => @forum_topic)
## subscribe to image
Subscription.create(:user => current_user, :subscribable => @image)

## fetching
@user.forum_topic_subscriptions
@user.image_subscriptions
@forum_topic.subscribers
@image.subscribers


I think that's all about right, it started to hurt my head heh. hope it helps smile

Last edited by mipr (2007-11-11 12:39:11)

Re: Two Similar, yet different Tasks...how to DRY?

mipr, thanks for the response. I will give it a try in an upcoming version. For now I decided to not let maximizing DRY stand in the way of developing quickly, and I went with two separate models, though I am sharing code for the mail notification.