All threads / How do I #create with a double delegated_type and has_many :through
Ask A Question

Notifications

You’re not receiving notifications from this thread.

How do I #create with a double delegated_type and has_many :through

Walther Diechmann asked in Rails

I have this marvelous complicated DB design that goes along these lines: events ( like calls, tasks, and meetings ) can have a number of assignees (Participants, and Assets) - like a Team Meeting can have a meeting room and the entire team of employees in team A assigned.

That one/two line 'use case' really has me in the ropes :cry:

class Event < ApplicationRecord
  belongs_to :calendar, optional: true
  has_many :assignments, inverse_of: :event

  delegated_type :eventable, types: %w[ Call Task Meeting], dependent: :destroy
end

class Task < Event
  include Eventable

  has_many :assignments, through: :event, inverse_of: :assignable
  accepts_nested_attributes_for :assignments
end 

class Assignment < ApplicationRecord
  belongs_to :event, inverse_of: :assignments
  delegated_type :assignable, types: %w[ Participant Asset ]
end

# routes.rb

  resources :employees do
     resources :tasks
  end

class TasksController < EventsController

  def new 
    @task= Task.new( event: Event.new)
    @task.assignments.new( assignable: (Employee.find(params[:employee_id]) rescue nil))
  end

  def create
   ??
  end


  private 
    def resource_params
      params.require(:task).permit(:duration, event_attributes: [ :name, :account_id ], assignments_attributes: [ :id, :assignable_id, :assignable_type, :assignable_role, :_destroy ]  )
    end

end

I am able to get a params back from the form with

#
# Parameters: {"authenticity_token"=>"[FILTERED]", 
# "task"=>{
#   "event_attributes"=>{"account_id"=>"1", "name"=>"meeting"}, 
#   "duration"=>"100", 
#   "assignments_attributes"=>{
#     "0"=>{"assignable_type"=>"Employee", "assignable_id"=>"54", "assignable_role"=>"owner"}
#     }
#   }
# }

I just don't get how I persist the thing :cry: (again)

Update: adding 'events' in the CLI

I "adjusted" the models somewhat (and added the eventable module for completeness, now looking like this:

class Event < ApplicationRecord
  belongs_to :calendar, optional: true
  has_many :assignments, inverse_of: :event

  delegated_type :eventable, types: %w[ Call Task Meeting], dependent: :destroy

  accepts_nested_attributes_for :eventable, reject_if: :all_blank, allow_destroy: true
  accepts_nested_attributes_for :assignments, reject_if: :all_blank, allow_destroy: true

end

class Task < Event
  include Eventable

  has_many :assignments, through: :event, inverse_of: :assignable
end 

module Eventable
  extend ActiveSupport::Concern

  included do
    has_one :event, as: :eventable, touch: true, dependent: :destroy

  end

end

class Assignment < ApplicationRecord
  belongs_to :event, inverse_of: :assignments
  delegated_type :assignable, types: %w[ Participant Asset ]
end


class TasksController < EventsController

  def new_resource 
    @event = Event.new eventable: Task.new
    @event.assignments.new( assignable: (Employee.find(params[:employee_id]) rescue nil))
  end

  def create
    @event = Event.new resource_params
  end


  private 


    # Never trust parameters from the scary internet, only allow the white list through.
    def resource_params
      params.require(:event).permit(:name, :account_id, eventable_attributes: [ :id, :duration ], assignments_attributes: [ :id, :assignable_id, :assignable_type, :assignable_role, :_destroy ]  )
    end

end


Further, I "recalibrated" the form to now produce this kind of params:

Started POST "/tasks" for 172.25.0.1 at 2022-03-29 17:40:30 +0000
hours-hours-1  | 17:40:30 web.1  | Processing by TasksController#create as TURBO_STREAM
hours-hours-1  | 17:40:30 web.1  |   Parameters: {"authenticity_token"=>"[FILTERED]", "event"=>{"account_id"=>"1", "name"=>"dfghjklæ", "eventable_attributes"=>{"duration"=>"110"}, "assignments_attributes"=>{"0"=>{"assignable_type"=>"Employee", "assignable_id"=>"54", "assignable_role"=>"owner"}}}}

(the somewhat 'funny' log is due to me running this all off a set of Docker containers)

While having the tasks_controller#create method doing the heavy-lifting still has quite a while to go - I've managed to fiddle the CLI into persisting a task by doing this:

irb(main):041:0> event = Event.new account_id: Account.first.id, name: "test", eventable: Task.new(duration: 100)
irb(main):044:0> event.assignments << Assignment.create( event: event, assignable: Employee.last, assignable_role: "owner")

Now - if only I could make the

irb(main):031:0> Event.new params["event"]
/usr/local/bundle/gems/activemodel-7.0.0/lib/active_model/attribute_assignment.rb:51:in `_assign_attribute': unknown attribute 'eventable_attributes' for Event. (ActiveModel::UnknownAttributeError)

go away - and take the log barfing up

hours-hours-1  | 17:40:30 web.1  | NoMethodError (undefined method `constantize' for nil:NilClass):
hours-hours-1  | 17:40:30 web.1  |   
hours-hours-1  | 17:40:30 web.1  | app/controllers/tasks_controller.rb:30:in `create_resource'

with it - I'd be home free :big_smile:

Update: inching my way through this - -

I decided to let the code rest while I finished Drive To Survive Season 4 which to a certain degree proved beneficial!

Tearing it all apart, I did:

    def create_resource
      r = resource_params.tap {|ary| ary.delete :eventable_attributes }
      r = r.tap {|ary| ary.delete :assignments_attributes}
      r = Event.new r, eventable: resource_params[:event][:eventable_attributes]
      r.assignments.build resource_params[:event][:assignments_attributes]
    end

which left me with this beauty:

hours-hours-1  | 22:58:01 web.1  | NoMethodError (undefined method `[]' for nil:NilClass):
hours-hours-1  | 22:58:01 web.1  |   
hours-hours-1  | 22:58:01 web.1  | app/controllers/tasks_controller.rb:33:in `create_resource'

() which at least left me with something to chase!

Update: every little step -- closer and yet so distant

Working on the issue had me redo the #create

    def create
      r = resource_params.tap {|ary| ary.delete :eventable_attributes }
      r = r.tap {|ary| ary.delete :assignments_attributes}
      r = Event.new r
      r.eventable = Task.new resource_params[:eventable_attributes]
      if r.valid?
        r.save
        resource_params[:assignments_attributes].each do |k,a|
          r.assignments << Assignment.create( event: r, assignable: a)
        end
      end
      @resource= r
    end

not in any way near my expectations - and it still won't let me off the hook

 r.assignments << Assignment.create( event: r, assignable: a)

(telling me that there is undefined method `primary_key' for ActionController::Parameters:Class)

Update: right about the ugliest method - where's the sugar?

I knew that no primary_key had this "wrong arguments" odour so it was a no-brainer to feed the beast some proper arguments

    def create
      r = resource_params.tap {|ary| ary.delete :eventable_attributes }
      r = r.tap {|ary| ary.delete :assignments_attributes}
      r = Event.new r
      r.eventable = Task.new resource_params[:eventable_attributes]
      if r.valid?
        r.save
        resource_params[:assignments_attributes].each do |k,a|
          r.assignments << Assignment.create( event: r, assignable_type: a["assignable_type"], assignable_id: a["assignable_id"], assignable_role: a["assignable_role"])
        end
      end
    end

so - it's a "done deal" (except it's nothing of the kind) but this was totally not what I expected from delegated_type (and I'm fully aware that I'm getting punished by my own sword)

Last update: getting my associations straight!

Well - like I more or less anticipated - the devil was lurking if not in the detail then in the assignment :big_smile:

Going back to the drawing board (and with a kind push from https://twitter.com/kaspth) I realized that I had been too generous with the delegated_typ'ing

class Assignment < AbstractResource

  belongs_to :event, inverse_of: :assignments
  belongs_to :assignable, polymorphic: true, required: true

  accepts_nested_attributes_for :assignable

when in fact assignments are nothing but mere (polymorphic) mortals :smile:

Now I could finally -- hunting this down for the better part of a week (jeez I'm getting too old for this game) -- have my abstracted_away controller (which will allow me to focus on all the crazy exciting methods and just c/p the "standard" controller stuff from inherited controllers like this (and I don't think it gets any thinner)

class TasksController < EventsController

  def new_resource 
    resource_class= Task
    super
  end

  private 

    # Never trust parameters from the scary internet, only allow the white list through.
    def resource_params
      params.require(:event).permit(:name, :account_id, :eventable_type, eventable_attributes: [ :duration ], assignments_attributes: [ :id, :assignable_id, :assignable_type, :assignable_role, :_destroy ]  )
    end

    #
    # implement on every controller where search makes sense
    # geet's called from resource_control.rb 
    #
    def find_resources_queried options
      Task.search Task.all, params[:q]
    end
end

Happy to report that all is - again - quiet on the West front (as opposed to the East front at the moment I'm afraid -- slava Ukraine btw)

Join the discussion

Want to stay up-to-date with Ruby on Rails?

Join 66,029+ developers who get early access to new tutorials, screencasts, articles, and more.

    We care about the protection of your data. Read our Privacy Policy.

    logo Created with Sketch.

    Screencast tutorials to help you learn Ruby on Rails, Javascript, Hotwire, Turbo, Stimulus.js, PostgreSQL, MySQL, Ubuntu, and more. Icons by Icons8

    © 2022 GoRails, LLC. All rights reserved.