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!

Bogdan Popa
  • 1,099
  • 1
  • 16
  • 37

7 Answers7

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.

ran632
  • 1,099
  • 1
  • 12
  • 14
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)

Richard Peck
  • 76,116
  • 9
  • 93
  • 147
  • I edited my question so you can see how I'm using my models and the new+create actions in my controller. – Bogdan Popa Aug 13 '14 at 08:54
  • 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 goes – Richard Peck Aug 13 '14 at 08:56
  • I get "dynamic constant assignment" from Client = params[:type].constantize (create action) – Bogdan Popa Aug 13 '14 at 08:58
  • 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. – Bogdan Popa Aug 13 '14 at 08:59
  • 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` table – Richard Peck Aug 13 '14 at 09:01
  • 1
    the error was coming from if @client.update(client_params). (I am talking about my original code) – Bogdan Popa Aug 13 '14 at 09:02
  • 1
    in rails console: Company Load (2.2ms) SELECT "clients".* FROM "clients" WHERE "clients"."type" IN ('Company') when calling Company.all – Bogdan Popa Aug 13 '14 at 09:03
  • See - from your logs - how the `Company` inherited model calls from your `clients` table *without* you having to set the `type`? – Richard Peck Aug 13 '14 at 09:05
  • I still get dynamic constant assignment.. :( – Bogdan Popa Aug 13 '14 at 09:13
  • Hmmm let me check again – Richard Peck Aug 13 '14 at 09:15
  • Try my new fix - if that doesn't work, we should go into chat – Richard Peck Aug 13 '14 at 09:19
  • thanks for your time. I finally got it to work. I'll post the answer here. – Bogdan Popa Aug 13 '14 at 12:45
  • @RichPeck can I use STI with nested form?, so I can create multiple record with different type in one form? in rails 4. – rails_id Dec 28 '15 at 03:52
  • 3
    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. – Brendon Muir Dec 01 '16 at 08:48
  • 2
    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. – Brendon Muir Dec 01 '16 at 08:50
  • 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 Company – scottysmalls Dec 02 '16 at 13:33
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
dnsh
  • 3,516
  • 2
  • 22
  • 47
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.

gangelo
  • 3,034
  • 4
  • 29
  • 43
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

davew
  • 1,255
  • 15
  • 27
0
private
def client_params
  params.require(params[:type].try(:downcase) || :client).permit(:type, :partner_id, :name, :email, :phone, :cui, :registration_id, :address) 
end
Bogdan Popa
  • 1,099
  • 1
  • 16
  • 37
  • 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. – Justin Wood Aug 13 '14 at 13:26
-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
  • 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. – jmshofstall May 25 '17 at 16:16