Rails STI and strong parameters
Asked Answered
S

7

6

I have a Client resource with 2 types: Person and Company.

routes.rb:

resources :clients
resources :people, :controller => "clients", :type => "Person"
resources :companies, :controller => "clients", :type => "Company"

clients_controller:

def new
  @client = Client.new()
  @client.type = params[:type]
end

def create
  @client = current_partner.clients.new(client_params)
  if @client.save
    redirect_to clients_path
  ...
end

...
private
def client_params
  params.require(:client).permit(:type, :partner_id, :name, :email, :phone, :cui,   :registration_id, :address)
end

def find_client
  @client ||= Client.find(params[:id])
end

client.rb

class Client < ActiveRecord::Base
  validates_presence_of :type
  CLIENT_TYPES = ['Person', 'Company']
end

person.rb

class Person < Client
  validates_presence_of :name, :email, :phone
end

compay.rb

class Company < Client
  validates_presence_of :name, :email, :cui, :registration_id, :phone
  validates_uniqueness_of :cui, :registration_id, uniqueness: {scope: :partner_id}
end

The problem is when I'm trying to edit a client's details and I submit the changes, I get param is missing or the value is empty: client. The route from where I'm getting this error is .../companies/3.

Any help on this noobie question? Thanks!

Sherwin answered 13/8, 2014 at 8:31 Comment(0)
G
8

You can disguise your inherited model to it's parent before the permit action by

def client_params

  if params.has_key? :person
    params[:client] = params.delete :person
  elsif params.has_key? :company
    params[:client] = params.delete :company
  end

  params.require(:client).permit(...)

end

Its just renaming the model's key in the params hash, and does the trick.

Gales answered 25/2, 2016 at 10:15 Comment(0)
A
6

Models

I think you're not using STI's properly

STI's are for Models, not controllers. As per the MVC programming pattern, your Models handle all the data-construction methodology. Your controllers are used as an interim between your user inputs & your views:

enter image description here

This means that if you want to use or create STI-driven functionality, you'll be best just using the backend classes for it (instead of manually passing type etc):

#app/models/person.rb
Class Person < Client
   ...
end

#app/models/company.rb
Class Company < Client
end

#app/models/client.rb
Class Client < ActiveRecord::Base
end

This will give you the ability to do this:

#config/routes.rb
resources :clients
resources :people,    controller: "clients", type: "Person"
resources :companies, controller: "clients", type: "Company"

#app/controllers/clients_controller.rb
Class ClientsController < ApplicationController
    def create
       model = get_model(params[:type])

       @model = model.new(model_params)
       @model.save
    end

    private

    def get_model type
       return type.singularize.titleize.camelize.constantize
    end

    def model_params
        params.require(params[:type].to_sym).permit(:client, :model, :attributes)
    end
end

This will the various STI Model that you've set by accessing /people or /companies, and then give you the ability to save data to it (which will save to the Client model with type set, of course)

Autosome answered 13/8, 2014 at 8:47 Comment(16)
I edited my question so you can see how I'm using my models and the new+create actions in my controller.Sherwin
Yeah, that further confirms my presumptions that you're not using the STI structure correctly. Try implementing what I proposed & let's see how that goesAutosome
I get "dynamic constant assignment" from Client = params[:type].constantize (create action)Sherwin
the new/create part of the clients worked correctly in terms of assigning the type. In rails console I can successfuly check a client's type by calling Client.last.type for example.Sherwin
Yeah but that's not real STI implementation - you need to create inherited models so you can call "Company.all", which will pull from the clients tableAutosome
the error was coming from if @client.update(client_params). (I am talking about my original code)Sherwin
in rails console: Company Load (2.2ms) SELECT "clients".* FROM "clients" WHERE "clients"."type" IN ('Company') when calling Company.allSherwin
See - from your logs - how the Company inherited model calls from your clients table without you having to set the type?Autosome
I still get dynamic constant assignment.. :(Sherwin
Hmmm let me check againAutosome
Try my new fix - if that doesn't work, we should go into chatAutosome
thanks for your time. I finally got it to work. I'll post the answer here.Sherwin
@RichPeck can I use STI with nested form?, so I can create multiple record with different type in one form? in rails 4.Rodeo
WARNING: You're basically allowing anyone with access to this controller action the ability to create an instance of any Ruby class they like on your server. What's worse is you're allowing them to then pass arguments to that class instance via the initialize method. You need to whitelist the value of params[:type] before calling constantize. Also, take a look at the classify method for converting strings into camel case class name strings.Claytonclaytonia
to follow up: the safest way to do this is TYPES = { 'person' => Person, 'company' => Company } in your class. Then Client::TYPES[params[:type]].new(params[:client]) will safely convert your params type into an actual class in one step or return nil. Calling new on nil will raise an error but that's better than the alternative and will signal some kind of interference anyway.Claytonclaytonia
I think you can avoid messing with type param and instantiating an instance of an unknown class by using the model name of your client object (assuming you set and validate it before the create and update method is called.) Something like : params.require(@client.model_name.to_s.underscore.to_sym).permit(.... will look like params.require(:person).permit(.... for Person and params.require(:company).permit(.... for CompanyTeheran
A
4

I have somewhat similar solution like Ran. But instead of changing params hash I am detecting the correct key which can be used in require method.

def client_params
  key = (params.keys & %w(person company))[0]
  params.require(key).permit(.....)
end
Animadversion answered 14/6, 2016 at 7:28 Comment(0)
S
1

The problem is that you're using the ClientsController to update a Company. The way you've set up your client_params method in your ClientsController, it's looking for :client in the params.require hash (params.require(:client)), but in the case of editing a Company, params.require(:client) is going to be missing; this is because what will be in your params.require hash would be params.require(:company), hence your error.

I think it's a bad idea to have one controller handle multiple models, and I also think it's a bad idea to do this, which has been suggested:

def client_params
  # This is not good, by virtue of the fact that you're using one controller
  # to handle multiple models.
  params.require(params[:type].try(:downcase) || :client).permit(:type, :partner_id, :name, :email, :phone, :cui, :registration_id, :address) 
end

It it were my code, I would have one controller for each model, and a base class (e.g. class BaseApplicationController < ApplicationController) to inherit from if I needed it. This would keep things DRY if there were any code that needed to be duplicated across your subclassed controllers. I then would simply provide a "client_params" method in each subclassed controller.

Just my humble opinion.

Salaried answered 17/3, 2015 at 18:35 Comment(0)
P
1

I found some great examples of strong params with STI here: https://gist.github.com/danielpuglisi/3c679531672a76cb9a91

You could change your client_params to:

def client_params(type)
  params.require(type.underscore.to_sym).permit(:type, :partner_id, :name, :email, :phone, :cui, :registration_id, :address)
end

Then your update action could be:

def update
    #assumes your `find_client` was run as a before action
    if @client.update(client_params(@client.type)
    ...
end

Then change your create action to pass in params[:type] into client_params

Polychaete answered 10/11, 2018 at 0:8 Comment(0)
S
0
private
def client_params
  params.require(params[:type].try(:downcase) || :client).permit(:type, :partner_id, :name, :email, :phone, :cui, :registration_id, :address) 
end
Sherwin answered 13/8, 2014 at 12:46 Comment(1)
You should accompany your answer with an explanation in order for the OP and future people to understand what it is doing. Even linking to the documentation would be useful.Narwhal
Q
-1

I had a similar issue where I have a Company model, a User model and Owner, Administrator, Analyst, etc subclasses, in my company model I have accepts_nested_attributes_for :user and the :user_attributes[:att, :att2] in the strong params permit, but what I needed to do was add the model name as a permitted parameter.

What I had at first

def company_params
    params.require(:company).permit(:coname, :industry, :users_attributes => [:id, :first_name, :last_name, :email, :password, :password_confirmation, :type])
end

Adding :owner, made it work for me.

def company_params
    params.require(:company).permit(:coname, :industry, :owner, :users_attributes => [:id, :first_name, :last_name, :email, :password, :password_confirmation, :type])
end
Quickly answered 25/5, 2017 at 16:14 Comment(1)
Also make sure you're in the right controller for the params require. I don't know how many times I've edited the wrong file thinking it was the right one.Quickly

© 2022 - 2024 — McMap. All rights reserved.