← Back to Upcase

Creating a controller for a model with a polymorphic association w/ individual models AND their nested model children


(Kevin Lozandier) #1

Problem

While understanding how to create a controller dealing with polymorphism that handles individual, independent models fine ; I’m having a hard time understand how to create such a controller that deals with models that aren’t independent, are children of other models that I also want the polymorphism controller be able to handle

Would I have to create a ‘nested’ variant of the controller or should I avoid attempting to force having to accomodate such deep nesting?

Suppose I have the following polymorphic association defined:


class Goals < ActiveRecord::Base 

belongs_to :goalable, polymorphic: true 
end 

Now suppose I have Project, Client, and Persona models with the following relationships with one another via routes (including having a have_many :goals defined inside each model :

#would create a concern to clean up the goal resources repition
resources :projects do 
  resources :goals, only: [:new,:create, :edit, :update]
  resources :personas do 
    resources :goals, only: [:new,:create, :edit, :update]
  end 
  resources :client do 
    resources :goals, only: [:new,:create, :edit, :update]
  end 
end


Tried looking up things online with no luck; Bates from Railscasts reviews the usual steps of creating a controller dealing with polymorphism, using the find_Xable pattern involving regular expressions, but he also doesn’t address the use case of models nested inside other models

Since I couldn’t do the usual pattern, I’ve attempted to do the following:

class GoalsController < ApplicationController 
  before_action :get_project
  before_action :set_up_goalable
  #before_action :get_existing_goal, only[:edit, :update, :destroy]
  def new
    @goalable.goals.new 
  end

  def create 
    @goal = @goalable.goals.new(goal_params)
    if @goal.save
      redirect_to project_path(@project), notice: "#{@goalable.class.to_s} goal successfully added"
 #for_now for get about redirecting to the @goalable path; probably can use id: nil for that. 
    
  end

  def edit
    @goal = @goalable.goals.find(params[:id])
  end

  def update 

  end


  private 

  def get_persona
    #get_project
    @project.find.personas.friendly.find(params[:persona_id])
  end

  def get_client  
    #get_project
    @project.find.personas.friendly.find(params[:client_id])
  end

  def goal_params 
    params.require(:goal).permit(:copy)
  end

  def get_project
    @project = Project.friendly.find(params[:project_id])
  end

  def set_up_goalable
    # hard assumption = if you know the params are project_id and persona_id or client_id exists, @goalable is either one (2) 
    # otherwise, get @project and see if there's another params with the x_id pattern to set to @goalable 
      # if that's nil, @project IS the @goalable. 
    if params[:project_id] && params[:persona_id]
      @goalable = get_persona
    elsif params[:project_id] && params[:client_id]
      @goalable = get_client 
    else
      # regular expression duty : find parameter ending with _id that's not 'project_id; otherwise  @goalable = get_project 
    end
  end
end

Note: I’m aware that famed Rubyists such as Jamis Buck would probably throw a golf club at me for trying to go deeper than one level with the current routing map; however, the only use of Persona and Client is with a Project; perhaps I should rethink that.

** If I were to do it that way, I would have a if/else clause that a project_id parameter exists to redirect differently than ‘normally’, but then a new problem of ensuring that parameter is passed in meaningfully as a hidden_field while also making sure a find_x_able pattern doesn’t pick up project_id by mistake as the model to create a goal for…**


(Kevin Lozandier) #2

Partial Solution

Biting the bullet and taking Jamis Buck’s approach to not deeply nest at least the polymorphic model (I don’t have problems with normal models deeply nested), I was able to make the association happen with correct redirecting of models:

With my routes decoupling the polymorphic model:

Personal::Application.routes.draw do

  concern :goalable do
    resources :goals, only, [:new, :create, :edit, :update]
  end

  resources :interests

  resources :technologies, only: [:index, :update, :edit, :create, :new, :destroy]

  root 'projects#index'

  resources :projects do 
    resources :identity_guidelines
    resources :photos, only: [:show, :new, :edit, :update, :destroy, :create]
    resources :clients 
    resources :attachments, only: [:new, :update, :destroy, :create]
    resources :goals, only: [:new, :create, :edit, :update]
    resources :technology_profiles, only: [:new, :edit, :update, :destroy, :create] 
    resources :personas do 
      resources :influencers
      #resources :goals, only: [:new, :create, :edit, :update]
      resources :interests, only: [:new, :create, :edit, :update, :destroy]
    end
  end

  resources :personas do 
    resources :goals, only: [:new, :create, :edit, :update]
  end

  resources :clients do
    resources :goals, only: [:new, :create, :edit, :update]
  end



end

I was able to then change the controller to use the usual find x_able pattern using regular expressions,as well as using respond_to? and eval, to solve the key models nested inside the Project model without having to redirect to the nested model’s (nonexistent) show and goals pages

class GoalsController < ApplicationController 
  #before_action :get_project
  def new
    @goalable = get_goalable 
  end

  def create 
    @goalable = get_goalable 
    @goal = @goalable.goals.build(goal_params)
    if @goal.save 
      if @goalable.respond_to?(:project) #had a case/when block using a different way before settling with this way with another if/else. 
        redirect_to eval("project_#{@goalable.class.to_s.downcase}_path(@goalable.project, @goalable)")
      else 
        redirect_to id: nil, notice: "Goal successfully added"
      end
    else 
      render :new 
    end 
  end

# Other CRUD methods

  private 

  def goal_params 
    params.require(:goal).permit(:copy)
  end

  def get_goalable
    params.each do |name, value|
      if name =~ /(.+)_id$/
        # in my case, 'friendly' worthy models will be worked on 
        return $1.classify.constantize.friendly.find(value) 
      end
    end
    nil 
  end
end

Remaining Questions

My remaining question is how to use something else than redirect_to :id that’s more explicit, and how to avoid using eval, which I thought was bad practice?


(Ben Orenstein) #3

Could this be what you’re looking for?

http://api.rubyonrails.org/classes/ActionDispatch/Routing/PolymorphicRoutes.html


(Kevin Lozandier) #4

@benorenstein : Yes it is from what I saw. I used @goalable as well, only to realize the limited milage I would have using that when I don’t necessarily want each @goalable model’s resource being able to be routed to outsie the scope of a nested resource.