How do I #create with a double delegated_type and has_many :through
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)