1

I have a form that enables a user to update multiple alert rules records at the same time. But each alert rule record can have many notification emails. So I would like to be able to update alert rules and the associated notification emails within the same form.

Models:

class AlertRule < ActiveRecord::Base

    has_many :notification_emails, :dependent => :destroy
    accepts_nested_attributes_for :notification_emails, :reject_if => lambda { |notification| notification[:email].blank? }
end

class NotificationEmail < ActiveRecord::Base
  belongs_to :alert_rule
end

In one of my controllers, I send an array of alerts to a form:

  def alerts_config

      //more code
      @alert_rules = alert_rules.flatten
      @alert_rules.each { |a| a.notification_emails.build }

      render :partial => 'home/alerts_config', :layout => false
  else

Then in my form, I want to allow the user to update alert rules and the associated email notifications:

    = form_for @alert_rules, :url => '/home/save_alerts_config/' + @unit.id.to_s, :remote => true, :class => 'ajaxForm' do |f|
  %table.scrollTable{:cellspacing => "0", :width => "740px", :style => "border-collapse: collapse; border-spacing: 0;"}
    %thead.fixedHeader
      %tr
        %th Alerts
        %th Enable
        %th Primary Email
        %th Notification Emails
        %th
    %tbody.scrollContent
      - for rule in @alert_rules
        %tr
          %td= rule.alert_code.name
          %td= check_box_tag "enabled_ids[]", rule.id
          %td= f.text_field :email, :value => rule.email, :index => rule.id
          %td.fields
            = f.fields_for :notification_emails, rule.notification_emails do |notification_builder|
              = notification_builder.text_field :email
              = notification_builder.hidden_field :_destroy              
              = link_to_function 'Remove Notification', 'remove_notifications(this)'
  .save_panel
    = submit_tag "Save", :class => 'submit myButton'

When I submit to server, this is what I get:

Started POST "/home/save_alerts_config/6243" for 127.0.0.1 at 2012-03-20 17:49:06 -0400
  Processing by HomeController#save_alerts_config as JS
  Parameters: {"utf8"=>"✓", "authenticity_token"=>"NPwuKuWippYjm2tJcfQI+/x9oEBwcR2rxcfpZMTO/Qo=", "enabled_ids"=>["51"], "alert_rule"=>{"51"=>{"email"=>"hythy"}, "notification_emails_attributes"=>{"0"=>{"email"=>"rtyrytry", "_destroy"=>"false"}, "1"=>{"email"=>"", "_destroy"=>"false"}, "2"=>{"email"=>"", "_destroy"=>"false"}, "3"=>{"email"=>"", "_destroy"=>"false"}, "4"=>{"email"=>"", "_destroy"=>"false"}, "5"=>{"email"=>"", "_destroy"=>"false"}, "6"=>{"email"=>"", "_destroy"=>"false"}, "7"=>{"email"=>"", "_destroy"=>"false"}, "8"=>{"email"=>"", "_destroy"=>"false"}, "9"=>{"email"=>"", "_destroy"=>"false"}, "10"=>{"email"=>"", "_destroy"=>"false"}, "11"=>{"email"=>"", "_destroy"=>"false"}, "12"=>{"email"=>"", "_destroy"=>"false"}, "13"=>{"email"=>"", "_destroy"=>"false"}, "14"=>{"email"=>"", "_destroy"=>"false"}, "15"=>{"email"=>"", "_destroy"=>"false"}, "16"=>{"email"=>"", "_destroy"=>"false"}, "17"=>{"email"=>"", "_destroy"=>"false"}, "18"=>{"email"=>"", "_destroy"=>"false"}, "19"=>{"email"=>"", "_destroy"=>"false"}, "20"=>{"email"=>"", "_destroy"=>"false"}, "21"=>{"email"=>"", "_destroy"=>"false"}, "22"=>{"email"=>"", "_destroy"=>"false"}, "23"=>{"email"=>"", "_destroy"=>"false"}, "24"=>{"email"=>"", "_destroy"=>"false"}, "25"=>{"email"=>"", "_destroy"=>"false"}, "26"=>{"email"=>"", "_destroy"=>"false"}, "27"=>{"email"=>"", "_destroy"=>"false"}, "28"=>{"email"=>"", "_destroy"=>"false"}, "29"=>{"email"=>"", "_destroy"=>"false"}, "30"=>{"email"=>"", "_destroy"=>"false"}, "31"=>{"email"=>"", "_destroy"=>"false"}, "32"=>{"email"=>"", "_destroy"=>"false"}, "33"=>{"email"=>"", "_destroy"=>"false"}, "34"=>{"email"=>"", "_destroy"=>"false"}, "35"=>{"email"=>"", "_destroy"=>"false"}, "36"=>{"email"=>"", "_destroy"=>"false"}, "37"=>{"email"=>"", "_destroy"=>"false"}, "38"=>{"email"=>"", "_destroy"=>"false"}, "39"=>{"email"=>"", "_destroy"=>"false"}, "40"=>{"email"=>"", "_destroy"=>"false"}}, "52"=>{"email"=>"yutu"}, "53"=>{"email"=>"ytuytu"}, "54"=>{"email"=>""}, "55"=>{"email"=>""}, "56"=>{"email"=>""}, "57"=>{"email"=>""}, "58"=>{"email"=>""}, "59"=>{"email"=>""}, "60"=>{"email"=>""}, "61"=>{"email"=>""}, "62"=>{"email"=>""}, "63"=>{"email"=>""}, "64"=>{"email"=>""}, "65"=>{"email"=>""}, "66"=>{"email"=>""}, "67"=>{"email"=>""}, "68"=>{"email"=>""}, "69"=>{"email"=>""}, "70"=>{"email"=>""}, "71"=>{"email"=>""}, "72"=>{"email"=>""}, "73"=>{"email"=>""}, "74"=>{"email"=>""}, "75"=>{"email"=>""}, "76"=>{"email"=>""}, "77"=>{"email"=>""}, "78"=>{"email"=>""}, "79"=>{"email"=>""}, "80"=>{"email"=>""}, "81"=>{"email"=>""}, "82"=>{"email"=>""}, "83"=>{"email"=>""}, "84"=>{"email"=>""}, "85"=>{"email"=>""}, "86"=>{"email"=>""}, "87"=>{"email"=>""}, "88"=>{"email"=>""}, "89"=>{"email"=>""}, "90"=>{"email"=>""}, "91"=>{"email"=>""}}, "commit"=>"Save", "id"=>"6243"}

This shouldnt be. As I show in the controller above, I only build one notification per alert, so why it sends all the notifications back as if they are all associated with the first alert, when I check the first alert only, is beyond me.

update: even when I used create! instead of build to actually write the associated record, it still presented same problem: unable to get the alert id inside the name attribute of the notification_email input field

Thanks for response

JohnMerlino
  • 3,900
  • 4
  • 57
  • 89
  • `fields_for` expects a collection object. When you call `all` you're feeding it an `Array`. What happens if you take the `all` off the call? So it looks like `, rule.notification_emails do` – Azolo Mar 19 '12 at 23:23
  • @Azolo I initially tried doing that but it gave me the same exact error message "ActionView::Template::Error (undefined method `email' for []:Array):" – JohnMerlino Mar 20 '12 at 00:00
  • I figured it wasn't that easy, I wonder what you're getting there instead. What does `= notification_builder.object.to_s` give you? – Azolo Mar 20 '12 at 01:18
  • @Azolo When I add both arguments: f.fields_for :notification_emails, rule.notification_emails notification_builder.object.to_s will return [#] and when I dont pass the second argument: f.fields_for :notification_emails, notification_builder.object.to_s will return an empty string. And when I use .all it gives me an empty array: [] – JohnMerlino Mar 20 '12 at 21:00
  • Maybe it has soething to do with fact that email notifications is not really an association yet (since i used build)n and so accepted_nested_attributes_for wont work: 1.9.2p290 :001 > AlertRule.first.notification_emails.build => # 1.9.2p290 :002 > AlertRule.first.notification_emails => [] – JohnMerlino Mar 20 '12 at 21:21
  • I think you're half right, but it's also the `form_for "alert_rules[]"` I think. – Azolo Mar 20 '12 at 21:38
  • Im going to update my question with what I get back when i use @alert_rules rather than "alert_rules[]" – JohnMerlino Mar 20 '12 at 21:53
  • I added this to alert rule model: def notification_emails_attributes=(attributes) logger.info "What is going on with this: #{attributes}" end but it doesnt get called or anything – JohnMerlino Mar 20 '12 at 22:14
  • I think I have it figured out. If you can post your old code up there too. Just as a reference for anyone else. – Azolo Mar 20 '12 at 22:45

1 Answers1

0

You're not going to like it. But this is what I did to make it work...

Information based on this stackoverflow question and was heavily influenced by this pretty awesome answer I can up with this:

models/alert_rules_set.rb

# NOTICE: I'm not inheriting from ActiveRecord::Base
class AlertRulesSet
  extend ActiveModel::Naming
  include ActiveModel::Conversion

  attr_accessor :alert_rules

  def alert_rules_attributes=(attributes)
    # I'm tricking "fields_for" here.
    # This should never actually be called
    raise
  end

  # Strip fields_for indexes and return an array
  def self.alert_rules_from(collection_set_hash)
    collection_set_hash[:alert_rules_attributes].values
  end

  def persisted?
    false
  end
end

Controller

def alerts_config
  //more code
  @alert_rules_set = AlertRulesSet.new
  @alert_rules_set = AlertRules.all # Or Whatever
  @alert_rules_set.alert_rules.each { |a| a.notification_emails.build }

  render :partial => 'home/alerts_config', :layout => false
end

def update_alerts_config
  @alert_rules = AlertRulesSet.alert_rules_from(params[:alert_rules_set])

  # Save Logic here
end

Form

= form_for @alert_rules_set, :url => '/home/save_alerts_config/' + @unit.id.to_s, :remote => true, :class => 'ajaxForm' do |f|
  %table.scrollTable{:cellspacing => "0", :width => "740px", :style => "border-collapse: collapse; border-spacing: 0;"}
  %thead.fixedHeader
    %tr
      %th Alerts
      %th Enable
      %th Primary Email
      %th Notification Emails
      %th
  %tbody.scrollContent
    = f.fields_for :alert_rules do |alert_rules_builder|
      %tr
        %td= rule.alert_code.name
        %td= check_box_tag "enabled_ids[]", rule.id
        %td= alert_rules_builder.text_field :email, 
                         :value => rule.email, :index => rule.id
        %td.fields
          = alert_rules_builder.fields_for :notification_emails do |notification_builder|
          = notification_builder.text_field :email
          = notification_builder.hidden_field :_destroy              
          = link_to_function 'Remove Notification',
                'remove_notifications(this)'.save_panel
= submit_tag "Save", :class => 'submit myButton'

Here's a link to a git branch where I got it working with another app and the commit compare.

This is crazy! What's going on?

So historically ActionPack tended to be pretty tightly coupled to ActiveRecord. This isn't a good idea for several reasons, so starting with Rails 3.0 ActiveModel was introduced. Unfortunately this is one of those cases that could probably be better.

ActiveModel

NOTE: Some of this is changing, specifically with this Rails 4 commit

So basically, I'm implementing the bare minimum to make this case work. Apart from the extend and include directives, the only thing that is necessary to make any other ActiveModel class play nice in this case seems to be persisted?.

As for everything else...

attr_accessor :alert_rules
This is just holding the alert_rules data. This is what fields_for is going to use to populate the fields with existing data.

alert_rules_attributes=(attributes)
This is kinda special. This is what fields_for checks for when it wants to know when to render a collection or not. Without this you will just get a single nested field(ie. [alert_rule][notification_emails_attributes]) instead of a collection of nested fields(ie. [alert_rule][notification_emails_attributes][1]).

self.alert_rules_from(collection_set_hash)
This is the more confusing part. So basically when you submit some nested attributes they're actually in a Hash with an index key.

nested_model_attributes = {
    "0"=>{"attribute"=>"This", "id"=>"0"},
    "1"=>{"attribute"=>"That"}
}

This method is a quick way to get rid of that and get usable data out (This happens in ActiveRecord too). I figured it would probably be easier to manipulate the data than try to do some saving magic in the AlertRuleSet class (but you could).

Community
  • 1
  • 1
Azolo
  • 4,353
  • 1
  • 23
  • 31
  • It looks good. Can you explain the purpose of self.users_from a little more, thanks. – JohnMerlino Mar 23 '12 at 21:43
  • Sorry! It was late last night, it's actually supposed to be alert_rules_from (for your specific case) but I guess I forgot to change it. But yeah I'll change it and explain why it's there. – Azolo Mar 23 '12 at 22:28
  • This was a lot of fun to research, but I feel like I'm not explaining some vital piece of information that isn't as basic as I thought. – Azolo Mar 24 '12 at 03:27