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)

Reply

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:

Reply

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!

Reply

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)

Reply

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)

Reply

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)

Reply
Join the discussion
Create an account Log in

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

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

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