lundi 11 juillet 2016

What is the correct way to handle nested forms for join tables in rails 3.X

I'm working on a rails app that ingests external configuration data in the form of a big JSON blob once an hour. My app has services which are acted upon automatically if certain constraints are satisfied. Constraints are specified as a pair (JSON path, acceptable value), and considered satisfied if the current JSON ingestion at the specified path equals the specified value. Services are not the only things in the database that reference constraints, and each of these different objects can have multiple constraints. For this reason constraints have their own model/table, and are referred to rather than referring to their owners which could be services or other objects. In addition there is a specially treated constraint on a service which controls notification urgency regarding that service, which is also specified as a constraint. Here are my models

class Constraint < ActiveRecord::Base
  attr_accessible path, value
end

class ServiceConstraints < ActiveRecord::Base
  belongs_to :service
  belongs_to :constraint, dependent: :destroy
  self.primary_key = :constraint

  attr_accesible :service_id, :constraint_id, :constraint_attributes
  accepts_nested_attributes_for :constraint
end

class Service < ActiveRecord::Base
  #a bunch of other stuff irrelevant to the question
  belongs_to :urgency_constraint, class_name: Constraint
  has_many :service_constraints, class_name: ServiceConstraint
  has_many :constraints, :through => :service_constraints, class_name: Constraint

  accepts_nested_attributes_for :urgency_constraint, allow_destroy: true
  accepts_nested_attributes_for :service_constraints, allow_destroy: true
  attr_accessible :name, :service_constraints_attributes
end

I'm trying to get the forms set up handle this, so that when I edit a service I can add one or more constraints to it directly, i.e. I can click 'add a constraint' and two new fields appear in which the user can enter the constraint path and value. Wash, rinse repeat as necessary. User's should also be able to change the contents of paths and values of constraints that already exist, and remove constraints entirely. I'm not concerned about duplicate (path, value) pairs in the constraints table.

The forms (in haml)

# app/views/services/_form.haml

= form_for(service) do |f|
-# stuff for the other fields
%table
  %thead
    %tr
      %td Constraint Path
      %td Constraint Values
      %td &nbsp;
  %tbody#service-constraints
    = f.fields_for :service_constraints do |ff|
      - render 'service_constraint_fields', f: ff
  %tfoot
    %tr
      %td
        - sac = service.additional_constraints.new
        - ff = instantiate_builder("service[service_constraints_attributes][0][constraint_attributes]", service.service_constraints.new, {})
        = link_to 'add a constraint', '#', 'data-insertion-node' => "#service-constraints", 'data-insertion-content' => CGI::escapeHTML(render 'service_constraint_fields', f: ff), 'class' => 'add-nested-fields'

# app/views/services/_service_constraint_fields.haml

%tr.service-constraint-fields
  = f.fields_for :constraint do |ff|
    %td
      = ff.text_field :path
    %td
      = ff.text_field :value
    %td
      = link_to "remove", "#", "data-removal-node" => ".service-constraint-fields", "class" => "remove-nested-fields"

I'm trying to do this "the rails way", which supposedly means I shouldn't need to modify my controller. The controller currently works perfectly as long as the constraints aren't used. Here begin my problems, and I have encountered several while attacking this from various angles

Firstly, as you can see, I'm using the "_service_constraint_fields" partial to populate data-insertion-content which is then dynamically inserted into the page using javascript when 'add a constraint' is clicked. But, because the ServiceConstraint object the form_builder is bound to has constraint_id=nil, the fields_for in the partail is iterating over an empty collection, so there are no <td> elements inserted, just an empty <tr>.

Secondly, I added a constraint and service_constraint join row to the database manually. They render in the form correctly, but when I save the form (without any modification), I get this error:

ActiveRecord::RecordNotFound - Couldn't find Constraint with ID=3 for ServiceConstraint with ID=:

This is with the following service_params submitted

{"name"=>"MyBogus", "service_constraints_attributes"=>{"0"=>{"constraint_attributes"=>{"path"=>"a.b.c", "value"=>"some value", "id"=>"3"}, "id"=>""}}}

I can't figure out why the service_constraint id field is blank.

I'm kinda new to rails, so if you need more info, just shout in the comments.

Aucun commentaire:

Enregistrer un commentaire