Katipo
Search  
Site Blog
  About  
  Home
About Portfolio Solutions Client Area Contact Us
: : About Us
Awards
Jobs
Our People
What Is A ... ?
Working From Home
News
Photo Gallery
Katipo Blog


Rails: Using has_many :through associations, and the piggyback plugin to model dynamic forms

Note: this relies on you using EdgeRails .

I’m writing an application that will allow administrators to create dynamic forms for topics. The “dynamic form” design boils down to many to many relationship between topic_type_fields and topic_types. After a lot of getting to know what is out there with modeling many to many relationships in Ruby on Rails I settled on the has_many :through route.

Both has_and_belongs_to_many and has_many :through weren’t completey up to the task, but has_many :through was clearly winning the “hearts and minds” war within the Rails community. It’s a more intuitive way of expressing the relationship and turns out to flexible enough to do what I need. With a little help from the piggy_back plugin and some guidance from hasmanythrough.com’s blog I was able to get what I wanted.

I wanted both the ability to have a many to many relationships and the acts_as_list mix-in functionality. That was the sort of tricky part to get DRY (and where piggy_back comes in).

What I wanted for each topic_type was emulation of the standard way to append a new a topic_type_field to the list of all the topic_type_fields associated with the topic_type. The fields should also be be able to be marked “required” and have their order in the form specified. Here’s what I came up with for the three table’s involved migrations:

create_table :topic_types do |t|
      t.column :name, :string, :null => false
      t.column :description, :text, :null => false
      t.column :created_at, :datetime, :null => false
      t.column :updated_at, :datetime, :null => false
end

create_table :topic_type_fields do |t|
      t.column :name, :string, :null => false
      t.column :description, :text
      ...
      t.column :created_at, :datetime, :null => false
      t.column :updated_at, :datetime, :null => false
end

# this is the join table, note it has a non-rails convention name
# which means we have to specify the name in our models
create_table :topic_type_to_field_mappings do |t|
      t.column :topic_type_id, :integer, :null => false
      t.column :topic_type_field_id, :integer, :null => false
      t.column :position, :integer, :null => false
      t.column :required, :boolean, :default => false
      t.column :created_at, :datetime, :null => false
      t.column :updated_at, :datetime, :null => false
end

Now looks at our models. First, topic_type.rb:

class TopicType < ActiveRecord::Base
  ...
  # notice the order by position, this is important for our acts_as_list functionality
  # even though we don't specify it in this model
  has_many :topic_type_to_field_mappings, :dependent => :destroy, :o rder => 'position'

  # you have to do the elimination of dupes through the sql
  # otherwise, rails will reorder by topic_type_to_field_mapping.id after the sql has been run
  has_many :form_fields, :through => :topic_type_to_field_mappings, :source => :topic_type_field, :select => "distinct topic_type_to_field_mappings.position, topic_type_fields.*", :o rder => 'position' do
    def <<(topic_type_field)
      # this allows us to say some_topic.form_fields << the_new_topic_type_field
      # to add a new associated topic_type_field to this topic_type
      TopicTypeToFieldMapping.with_scope(:create => { :required => "false"}) { self.concat topic_type_field }
    end
  end
  has_many :required_form_fields, :through => :topic_type_to_field_mappings, :source => :required_form_field, :select => "distinct topic_type_to_field_mappings.position, topic_type_fields.*", :conditions => "topic_type_to_field_mappings.required = 'tru\e'", :o rder => 'position', :uniq => true do
    def <<(required_form_field)
      # this allows us to say some_topic.required_form_fields << the_new_topic_type_field
      # to add a new associated topic_type_field to this topic_type
      # but have topic_type_to_field_mappings.required column set to true
      TopicTypeToFieldMapping.with_scope(:create => { :required => "true"}) { self.concat required_form_field }
    end
  end
  ...
end

Note: a good bit of my explanation is as comments in the code, be sure to read it.

Next up, topic_type_field.rb:

class TopicTypeField < ActiveRecord::Base
  has_many :topic_type_to_field_mappings, :dependent => :destroy
  ...
end

Not much to see there. The last bit of magic is in join model in topic_type_to_field_mapping.rb:

class TopicTypeToFieldMapping < ActiveRecord::Base
  belongs_to :topic_type
  belongs_to :topic_type_field
  belongs_to :form_field, :class_name => "TopicTypeField", :foreign_key => "topic_type_field_id"
  belongs_to :required_form_field, :class_name => "TopicTypeField", :foreign_key => "topic_type_field_id"

  # note that this is where we specify the acts_as_list functionality
  # not on some_topic_type.form_fields or some_topic_type.required_form_fields
  # these will be ordered by position, but won't have the acts_as_list methods
  acts_as_list :scope => :topic_type_id

  # in the context of wanting to see all list of all of a topic_type's field mappings (the join model, thus only it's attributes such as position and required)
  # which as noted above is where the acts_as_list methods are available
  # we also want to see the topic_type_field's name and description attributes
  # we do this using the piggy_back plugin which effectively writes the join sql we need to get it
  piggy_back :topic_type_field_name_and_description,
      :from => :topic_type_field, :attributes => [:name, :description]
end

So what does this mean? It allowed me to easily add actions to my topic_types_controller.rb that would add set of topic_type_fields as either optional or required fields via the “<<” syntax and create a form that would update the position of the field in the topic_type’s dynamic form. Here’s a snippet:

<h4>Current Form Fields</h4>
...
                        <% # required, position, and acts_as_list functionality are in mappings
                           # but we also need field name attribute from topic_type_fields
                           # we use piggy_back plugin to get the name attribute, see model
                        %>
                        <% @form_field_mappings = TopicType.find(local_id).topic_type_to_field_mappings %>
                        <% if @form_field_mappings.size > 0 %>
                                <%= form_tag :action => :reorder_fields_for_topic_type, :id => local_id %>
                                        <% for @mapping in @form_field_mappings %>
                                                <p>
                                                        <%= text_field("mapping[]", 'position', :size => "3") %>
                                                        <%= h(@mapping.topic_type_field_name) %>
                                                        <% if @mapping.required %>
                                                                (required)
                                                        <% else %>
                                                                (optional)
                                                        <% end %>
                                                </p>
                                        <% end %>
                                        <%= submit_tag "Reorder Topic Type Form" %>
                                <%= end_form_tag %>
                        <% else %>
                                <p>No form fields currently mapped to this topic type.</p>
                        <% end %>
...

The acts_as_list functionality isn’t really used yet, but with it I should be able to add an AJAXified version which does position resorting via “Drag ‘n Drop” as outlined in this pdf.

Enjoy,
Walter

3 Responses to “Rails: Using has_many :through associations, and the piggyback plugin to model dynamic forms”

  1. skwasha Says:

    What does your topic_type controller look like then in the case where you are adding a list of topic_type_fields? did you ever implement the acts_as_list stuff?

  2. walter Says:

    Yes. They will be apart of the Kete project when it is released. You can find out more here: http://kete.net.nz/ .

    Cheers,
    Walter

  3. walter Says:

    Note on the first note: this code also works with Rails 1.2 RC1.

    Cheers,
    Walter

Leave a Reply

You must be logged in to post a comment.


Katipo
Rachel Snowboarding