kpaul
kpaul

Reputation: 479

Polymorphic and has_one association

I have set up a polymorphic association in my app. A picture belongs to both a scoreboard model and a user model as pictureable. Each scoreboard and user has_one picture. I also have nested routes where pictures resource is nested inside a user and scoreboard resource.

Routes File for the users:

resources :users do
  resources :picture
end

resources :account_activations, only: [:edit]
resources :password_resets, only: [:new, :create, :edit, :update]

resources :conversations, only: [:index, :show, :destroy] do
  member do
    post :reply
    post :restore
    post :mark_as_read
  end

  collection do
    delete :empty_trash
  end
end

Routes file for the scoreboard:

resources :scoreboards do 
  member do
    put :favourite
    get :deleteteams
    get :deleteschedules
  end

  resources :invitations, only: [:new, :create]
  resources :comments
  resources :teams, only: [:edit, :create, :destroy, :update]
  resources :schedules 
  resources :picture
end

I have setup a picture controller. The code is given below.

class PictureController < ApplicationController
  before_filter :load_pictureable

  def new 
    @picture = @pictureable.build_picture                              
  end                                               

  def create
  end

  def destroy
  end

  private

  def picture_params
    params.require(:picture).permit(:picture)
  end

  def load_pictureable
    klass = [User, Scoreboard].detect { |c| params["#{c.name.underscore}_id"] }
    @pictureable = klass.find(params["#{klass.name.underscore}_id"]) 
  end
end                           

My new form is given below:

 <div id= "uploadphoto">
   <%= form_for([@pictureable, @picture], url: scoreboard_picture_path, html: {multipart: true}) do |f| %>
     <%= f.file_field :picture, accept: 'image/jpeg,image/gif,image/png', id: "files", class: "hidden" %>
     <label for="files" id="picture"><h4 id="picturetext">Click Here to Upload Picture</h4></label>
       <div class="modal" id="modal-1">
         <div class="modal-dialog">
           <div class="modal-content">
             <div class="modal-header">Position and Crop your Image</div>
             <div class="modal-body">
               <div class="upload-preview">
                <img id="image" /></img>
               </div>
               <%= f.submit "upload photo" %>
             </div>
           <div class="modal-footer"> Click Anywhere Outside the box to Cancel</div>
         </div>
       </div>
     </div>
  <% end %>
</div>

The problem I am having is with the URL. This gives me the following error.

No route matches {:action=>"show", :controller=>"picture", :scoreboard_id=>"13"} missing required keys: [:id]

I am not sure why it redirects me to the show action, I assume its probably the nested routes.I don't know what the problem is. I thought I had resolved it by passing the following URL new_scoreboard_picture_path. However, when I press upload photo, It would take me back to the same URL. This makes sense because I am explicitly stating the URL in the new form. However, that is incorrect. It would be a great help if someone could help me figure out what the issue is. I have only included the relevant code. However, if i am missing anything please do let me know. Any explanation as to why I am getting this error would also clarify my though process. This is the first time working with polymorphic associations. Any sort of help would be great!!

Upvotes: 1

Views: 247

Answers (1)

max
max

Reputation: 102433

I would use inheritance and two separate controllers instead:

# the base controller
class PicturesController < ApplicationController
  before_action :load_pictureable

  def new 
    @picture = @pictureable.build_picture                              
  end                                               

  def create
    @picture = @pictureable.build_picture
    if @picture.save
      do_redirect
    else
      render_errors
    end
  end

  def destroy
    # ...
  end

  private

  def picture_params
    params.require(:picture).permit(:picture)
  end

  # Finds the picturable based on the module name of the class
  # for examples `Pets::PicturesController` will look for
  # Pet.find(params[:pet_id])
  def find_pictureable
    # this looks at the class name and extracts the module name
    model_name = self.class.deconstantize.singularize
    @pictureable = model_name.constanize.find(params["#{model_name}_id"])
  end

  # subclasses can override this to whatever they want
  def do_redirect
    redirect_to @pictureable
  end

  # subclasses can override this to whatever they want
  def render_errors
    render :new
  end
end

Then setup the routes:

resources :users do
  resource :picture, controller: 'users/pictures'
end

resources :scoreboards do
  resource :picture, controller: 'scoreboards/pictures'
end

Since a user/scoreboard can have only one picture we are using resource and not resources. This sets up the routes for singular resource. Use rake routes to see the difference for yourself. For example you create a picture with POST /users/:user_id/picture.

We then setup the child controllers:

# app/controllers/users/pictures_controller.rb
class Users::PicturesController < PicturesController
end

 # app/controllers/scoreboards/pictures_controller.rb
class Scoreboards::PicturesController < PicturesController
end

This allows a high degree of customization without a jumble of if/else or switch statements. Also this safeguards against a malicious user passing params[:user_id] to manipulate a users picture. You should never use user input to determine classes or database tables.

Also you do not need to explicitly specify the form url:

<%= form_for([@pictureable, @picture], html: {multipart: true}) do |f| %>

Will use users_picture_path(user_id: @pictureable.id) or the same for scoreboards.

Upvotes: 0

Related Questions