Topic: Limiting the number of polymorphic joins

Hey, I am building an app in which the Photo model (I am using file column fyi) is joined polymorphically to Users and Posts. I have got the CRUD actions for the Photo model working prefectly well, but I want to limit the number of photos a user can upload to 4 for example.

In Environment.rb

LIMIT_ON_NUMBER_OF_PHOTOS_PER_ATTACHMENT = 4

Post.rb (simplified, obviously)
class Post < ActiveRecord::Base
  has_many :photos, :as => :attachment, :dependent => :destroy, :order => :position
end

User.rb
class User < ActiveRecord::Base
  has_many :photos, :as => :attachment, :dependent => :destroy, :order => :position
end

Photo.rb (simplified)
class Photo < ActiveRecord::Base
  belongs_to :attachment, :polymorphic => true
  file_column :image
  validate :at_or_over_limit_for_number_of_photos?
 
  def at_or_over_limit_for_number_of_photos?
    if self.attachment.photos.length >= LIMIT_ON_NUMBER_OF_PHOTOS_PER_ATTACHMENT
      errors.add_to_base "You can only upload up to #{LIMIT_ON_NUMBER_OF_PHOTOS_PER_ATTACHMENT} photos."
    end
  end
 
end

The migration for the Photo model looks like this:
class CreatePhotos < ActiveRecord::Migration
  def self.up
    create_table :photos do |t|
      t.column :id, :integer, :auto_increment => true
      t.column :caption, :string, :default => "", :null => false
      t.column :image, :string, :null => false, :default => ""
      t.column :attachment_id, :integer
      t.column :attachment_type, :string
      t.column :position, :integer
    end
  end

  def self.down
    drop_table :photos
  end
end


I am getting the following error with the above validation when I try to add a photo to a user (irrespective of whether or not the user is over the limit:
NameError in Admin photosController#create
"user" is not a valid constant name!

but if I change it to
def at_or_over_limit_for_number_of_photos?
    if self.user.photos.length >= LIMIT_ON_NUMBER_OF_PHOTOGRAPHS_PER_ATTACHMENT
      errors.add_to_base "You can only upload up to #{LIMIT_ON_NUMBER_OF_PHOTOGRAPHS_PER_ATTACHMENT} photos.  Please delete an existing photo before uploading another."
    end
  end

and try to upload a photo for a user I get a no method error.

oh and the controller actions look like this:

  def new
    @photo = Photo.new
    @photo.attachment_type = params[:attachment_type]
    @photo.attachment_id = params[:attachment_id]
  end

  def create
    @photo = Photo.new(params[:photo])
    if @photo.save
      flash[:notice] = "Added image"
    else
      flash[:notice] = "Didn't work out."
    end
    redirect_to :action => 'index'
  end


Thanks folks! Any help would be greatly appreciated.

Last edited by alright.dylan (2007-01-24 11:01:42)

Re: Limiting the number of polymorphic joins

I see the attachment_type column is being set directly in the "new" action. The type needs to match the name of the class exactly (case sensitive). Make sure the type is set to "User" instead of "user".

Railscasts - Free Ruby on Rails Screencasts

Re: Limiting the number of polymorphic joins

D'oh! Thanks, ryan, it now works! Out of curiousity (and the desire to improve my application design) would you have added the type and id some other way?

Thanks again for everything.

Last edited by alright.dylan (2007-01-24 16:02:55)

Re: Limiting the number of polymorphic joins

It depends on the interface of the application. As long as the user isn't interacting with this value directly then I think it's fine to do it this way. Just keep in mind the user can insert any value as the type by altering the URL. I don't see any obvious security problems with this, but just something to be aware of.

Railscasts - Free Ruby on Rails Screencasts

Re: Limiting the number of polymorphic joins

Thanks, Ryan, as a follow up, though, how would I go about overriding this validation from one of a controller's actions?

That is to say, I'd like to be able to put 'something' in the controller action and then override the 'at_or_over_limit_for_number_of_photos?' validation, but I am having the darnedest time trying to phrase it correctly. Any help would be awesome.

Last edited by alright.dylan (2007-01-25 22:11:20)

Re: Limiting the number of polymorphic joins

This is a common problem without an easy solution. You can bypass all validation by sending "false" as a parameter in the save method:

@photo.save(false) # I think that works anyway

But, if you want to be specific on which validations to bypass this will need to be handled by the model. In other words, you would have the controller call a method on the model which would set an instance variable. The model would then have an "if" condition determining whether to do the validation based on the value of the instance variable.

Railscasts - Free Ruby on Rails Screencasts

Re: Limiting the number of polymorphic joins

Well, it's not for lack of trying, but I can't quite wrap my mind around how to do this; it sounds clear enough conceptually, but my ruby isn't up to snuff. Going to buy Ruby for Rails (heard good things about it), but in the meantime, I would still really dig any more help if you all have it in you.

My model looks a little like this:

class Photo < ActiveRecord::Base
  validate :at_or_over_limit_for_number_of_photos?

  def skip_it
    return unless #something!
  end
 
  def at_or_over_limit_for_number_of_photos?
    if skip_it and self.attachment.photos.length >= LIMIT_ON_NUMBER_OF_PHOTOGRAPHS_PER_ATTACHMENT
      errors.add_to_base ("You can only upload up to #{LIMIT_ON_NUMBER_OF_PHOTOGRAPHS_PER_ATTACHMENT} photos.  Please delete an existing photo before uploading another.")
    end
  end
end


and the controller would look thusly:
def update
    @photo = Photo.find(params[:id])
      if @photo.update_attributes(params[:photo])
        flash[:notice] = 'Photo was successfully updated.'
        redirect_to :action => 'index'
      else
        render :action => 'edit', :id => @photo
     end
  end

I just can't figure out how the update action would call the skip_it model function or what skip_it should in fact look like. Thanks.

Last edited by alright.dylan (2007-01-26 01:10:39)

Re: Limiting the number of polymorphic joins

You would have the skip_it method set an instance variable. Just for brevity you can make an accessor for this:

class Photo < ActiveRecord::Base
  attr_accessor :skip_it
  #...
    def at_or_over_limit_for_number_of_photos?
    if !@skip_it and self.attachment.photos.length >= LIMIT_ON_NUMBER_OF_PHOTOGRAPHS_PER_ATTACHMENT
      errors.add_to_base ("You can only upload up to #{LIMIT_ON_NUMBER_OF_PHOTOGRAPHS_PER_ATTACHMENT} photos.  Please delete an existing photo before uploading another.")
    end
  end
end

You would set this in the controller like:

  def update
    @photo = Photo.find(params[:id])
    @photo.skip_it = true
    if @photo.update_attributes(params[:photo])
      flash[:notice] = 'Photo was successfully updated.'
      redirect_to :action => 'index'
    else
      render :action => 'edit', :id => @photo
    end
  end

You may want to give the @skip_it variable a better name.

Railscasts - Free Ruby on Rails Screencasts

Re: Limiting the number of polymorphic joins

Awesome! Thanks, Ryan. Works perfectly and I really appreciate it. When you say to give the variable a better name do you mean a more descriptive name or is there a convention I am unaware of? thanks again!

Re: Limiting the number of polymorphic joins

A more descriptive name specifying what it is skipping. There's no conventions involving this that I'm aware of.

Railscasts - Free Ruby on Rails Screencasts