Вложенная форма с has_many через существующие записи - PullRequest
0 голосов
/ 30 марта 2019

В моем приложении есть клиенты и контакты. Клиенты могут иметь много контактов, а контакты могут иметь много клиентов. Они связаны через таблицу CustomerContact. Дело в том, что в базе данных может быть только один уникальный контакт на основе их адреса электронной почты.

Что, я хочу создать форму, в которой пользователь может ввести информацию о клиенте и столько контактов, сколько он хочет. Я выполняю это, используя коконовый камень . Теперь дело в том, что здесь нет «выбрать существующий контакт», есть только текстовые поля, и идея заключается в том, что при отправке формы система может увидеть, существует ли уже контакт в системе через существующий адрес электронной почты, а не только назначить клиента на контакт, но и обновить существующую контактную информацию. Если это новый контакт, он просто вставит новый контакт в базу данных и назначит контакт клиенту.

Теперь, очевидно, это совершенно за пределами обычного способа ведения дел. Обычно у вас есть какой-то поиск и затем выбирается существующий контакт, но это не то, что хочет босс.

Вот проблема, с которой я сталкиваюсь

Rails всегда выдавал сообщение об ошибке «Адрес электронной почты уже занят», когда я вводил адрес электронной почты для контакта, существующего в базе данных. «Не проблема, я думал», для существующих контактов, я буду искать в базе данных и создавать атрибут id для контакта, манипулируя параметрами в контроллере. Поэтому я написал код ниже:

# convert the params to a Hash so we can manuipulate them
myparams = customer_params.to_h

# if we have contacts we want to assign to the customer
if myparams['contacts_attributes']
  # loop through each contact they entered
  myparams['contacts_attributes'].each do |k,v|
    # see if the contact exists in the database and isn't being destroyed
    if Contact.exists?(email: v['email']) && v['_destroy'] != '1'
      # grab the contact information
      contact = Contact.where(email: v['email']).first
      # just a double check that we got the contact from the database
      if contact
        # create an id attribute for the contact
        myparams['contacts_attributes'][k]['id'] = contact.id
      end
    end
  end
end

"Красивые !!!" или так я думал. Когда я попытался сохранить контакт, меня встретила следующая ошибка:

Couldn't find Contact with ID=117 for Customer with ID=

Очевидно, что при передаче параметра методу Customer#new Rails выполняет поиск в таблице CustomerContact, чтобы попытаться получить доступ к таблице контактов, чтобы получить информацию для контакта. Однако, поскольку это новый Заказчик, эта связь еще не установлена, поскольку Заказчик не был создан.

Хорошо ... Итак, у меня появилась идея

Что если я удалил существующие контакты из contact_attributes и сразу создал ассоциацию customer_contacts !!! Итак, где myparams['customer_contacts_attributes'] = [] вступает в игру:

# convert the params to a Hash so we can manuipulate them
myparams = customer_params.to_h
# so we can create a record on the customer_contacts association
# directly if the contact already exits
myparams['customer_contacts_attributes'] = []    

# if we have contacts we want to assign to the customer
if myparams['contacts_attributes']
  # loop through each contact they entered
  myparams['contacts_attributes'].each do |k,v|
    # see if the contact exists in the database and isn't being destroyed
    if Contact.exists?(email: v['email']) && v['_destroy'] != '1'
      # grab the contact information
      contact = Contact.where(email: v['email']).first
      # just a double check that we got the contact from the database
      if contact
        # removed the contact
        myparams['contacts_attributes'].delete(k)
        # create the `customer_contact` association directly
        myparams['customer_contacts_attributes'].push({'user_id': contact.id})
      end
    end
  end
end

И ... ЭТО РАБОТАЛО !!!! Ну кое что. Клиент сохраняется, контакты, которые уже существуют в базе данных, назначаются, а новые контакты создаются и назначаются ... так в чем же тогда проблема ??? Хорошо ... если проверка не удалась по какой-либо причине и страница перерисовывается, существующий контакт, введенный пользователем, исчезает из формы.

Моя просьба о помощи

Так что у меня есть кое-что, что работает, но мне действительно нужна помощь. Мне нужно, чтобы в случае сбоя проверки контакт снова отображался в форме. Кроме того, очевидно, что это не лучший способ сделать это. Я надеюсь, что кто-то уже делал что-то подобное в рельсах, и у него есть лучший способ сделать это.

Ниже приведены настройки моей ассоциации:

class CustomerContact < ApplicationRecord
  belongs_to :customer
  belongs_to :contact
end

class Customer < ApplicationRecord
  has_many :customer_contacts
  has_many :contacts, through: :customer_contacts

  accepts_nested_attributes_for :customer_contacts
  accepts_nested_attributes_for :contacts, allow_destroy: true
end

class Contact < ApplicationRecord
  has_many :customer_contacts
  has_many :customers, through: :customer_contacts
end

Ниже приведена настройка раздела контактов, обратите внимание, что я использую это как в новых, так и в редактируемых действиях:

<table class="table table-striped">
  <thead>
  <tr>
    <th>Name</th>
    <th>Email</th>
    <th>Phone</th>
    <th>Department</th>
    <th>Manager</th>
    <th>
      <%= link_to_add_association f, :contacts, class: 'btn btn-primary', partial: 'contact_fields', data: {
          association_insertion_node: '.contact_fields', association_insertion_method: :append
      } do %>
          <i class="fas fa-plus"></i>
      <% end %>
    </th>
  </tr>
  </thead>
  <tbody class="contact_fields">
  <%= f.fields_for :contacts do |contact| %>
      <%= render 'projects/contact_fields', f: contact %>
  <% end %>
  </tbody>
</table>

contact_fields частичное ниже:

<tr class="nested-fields">
  <td>
    <%= f.text_field :fullname, class: 'form-control invoke-contacts-search contact-fullname' %>
  </td>
  <td>
    <%= f.text_field :email, class: 'form-control invoke-contacts-search contact-email' %>
  </td>
  <td>
    <%= f.text_field :phone, class: 'form-control contact-phone' %>
  </td>
  <td>
    <%= f.text_field :department, class: 'form-control contact-department' %>
  </td>
  <td>
    <%= f.text_field :manager, class: 'form-control contact-manager' %>
  </td>
  <td>
    <%= link_to_remove_association  f, class: 'btn btn-danger' do %>
        <i class="fas fa-trash-alt"></i>
    <% end %>
  </td>
</tr>

Вот контроллер new и create action

def new
  @customer = Customer.new
  4.times { @customer.contacts.build }
end

def create

# convert the params to a Hash so we can manuipulate them
myparams = customer_params.to_h
# so we can create a record on the customer_contacts association
# directly if the contact already exits
myparams['customer_contacts_attributes'] = []    

# if we have contacts we want to assign to the customer
if myparams['contacts_attributes']
  # loop through each contact they entered
  myparams['contacts_attributes'].each do |k,v|
    # see if the contact exists in the database and isn't being destroyed
    if Contact.exists?(email: v['email']) && v['_destroy'] != '1'
      # grab the contact information
      contact = Contact.where(email: v['email']).first
      # just a double check that we got the contact from the database
      if contact
        # removed the contact
        myparams['contacts_attributes'].delete(k)
        # create the `customer_contact` association directly
        myparams['customer_contacts_attributes'].push({'user_id': contact.id})
      end
    end
  end
end

  @customer = Customers.new(myparams)

  respond_to do |format|
    if @customer.save
      format.html { redirect_to edit_customer_path(@customer), success: 'Customer was successfully created.' }
    else
      format.html { render :new }
    end
  end
end

Вот действия по редактированию и обновлению контроллеров

def edit
  @customer = Customer.find(params[:id])
end

def update
  myparams = customer_params.to_h

  if myparams['contacts_attributes']
    myparams['contacts_attributes'].each do |k,v|
      if Contacts.exists?(email: v['email']) && v['_destroy'] != '1'
        contact = Contact.where(email: v['email']).first
        if contact
          myparams['contacts_attributes'][k]['id'] = contact.id
          CustomerContact.find_or_create_by(project_id: @customer.id, user_id: contact.id)
        end
      end
    end
  end

  @customer.assign_attributes(myparams)

  respond_to do |format|
    if @customer.save
      format.html { redirect_to edit_customer_path(@customer), success: 'Customer was successfully updated.' }
    else
      format.html { render :edit }
    end
  end
end

1 Ответ

0 голосов
/ 02 апреля 2019

Итак, я наконец-то заставил это работать, но мне пришлось переопределить все, что вложенные атрибуты делают для вас, чтобы заставить это работать.Это не красиво, но работает отлично.

Также ... Посмотрите на этот пост , чтобы увидеть, как я сделал коллекцию контактов в шаблоне

  def update

    myparams = customer_params.to_h
    contacts_valid = true
    @customer.assign_attributes(myparams)
    customer_valid = @customer.valid?
    @contacts = []
    contacts_to_delete = []

    cparams = contact_params.to_h
    cparams['contacts'].each do |k, v|
      if v['id'].to_i > 0
        if v['_destroy'].to_i == 1
          contacts_to_delete << v['id'].to_i
        else
          contact = Contact.find(v['id'])
          contact.assign_attributes(v.except('_destroy', 'id'))
        end
      else
        if (!v['name'].blank? || !v['email'].blank?) && v['_destroy'].to_i != 1
          unless v['email'].blank?
            contact = Contact.where(email: v['email']).first
          end
          if contact
            contact.assign_attributes(v.except('_destroy', 'id'))
          else
            contact = Contact.new(v.except('_destroy', 'id'))
          end
        end
      end

      if contact
        unless contact.valid?
          contacts_valid = false
          # This adds the errors from the contact to the customer object
          contact.errors.full_messages.each do |m|
            @customer.errors.add(:base, m)
          end
        end
        @contacts << contact
      end
    end

    respond_to do |format|
      if customer_valid && contacts_valid
        @customer.save!
        @contacts.each do |c|
          c.save!
          CustomerContact.find_or_create_by(customer_id: @customer.id, contact_id: c.id)
        end
        CustomerContact.where(customer_id: @customer.id, contact_id: contacts_to_delete).destroy_all

        format.html { redirect_to edit_customer_path(@customer), success: 'Customer was successfully updated.' }
        format.json { render :show, status: :ok, location: @customer }
      else
        format.html {
          (6 - @contacts.count).times { @contacts << @customer.contacts.build }
          render :edit
        }
        format.json { render json: @customer.errors, status: :unprocessable_entity }
      end
    end

  end

  def customer_params
    params.require(:customer).permit(:company, :name, :line1, :line2, :city, :state, :zipcode, :country, :webaddress, :status,
                                     contacts_attributes: [:fullname, :phone, :email, :department, :manager, :id, :_destroy],
                                     notes_attributes: [:note]
    )
  end

  def contact_params
    params.permit(contacts: [:fullname, :email, :phone, :department, :manager, :id, :_destroy])
  end
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...