Проблеми на Rails 4 с актуализиране/изтриване на полета в модел с вложени полета

Работя върху въвеждането на вложена форма в моето приложение след епизод 196 на Railscast http://railscasts.com/episodes/196-nested-model-form-revised и ремастерираната версия за rails 4 https://github.com/dnewkerk/nested-model-form.

Да приемем, че имаме връзка 1 към много между разписки и статии.

Ето как изглеждат техните модели:

receipt.rb:

class Receipt < ActiveRecord::Base
  has_many :articles, dependent: :destroy
  accepts_nested_attributes_for :articles, allow_destroy: true, reject_if: :all_blank

  belongs_to :shop
  belongs_to :user

  def display_name
    self.name
  end
end

article.rb:

class Article < ActiveRecord::Base
  belongs_to :receipt

  def name_with_brand
    "#{name} #{brand}"
  end
end

Ето как изглежда receipts_controller.rb:

class ReceiptsController < ApplicationController
  before_action :set_shop, only: [:show, :edit, :update, :destroy]

  respond_to :html, :xml, :json

  def index
    @receipts = current_user.receipts
    respond_with(@receipts)
  end

  def show
    respond_with(@receipt)
  end

  def new
   @receipt = Receipt.new
   2.times do
     @receipt.articles.build
   end
   respond_with(@receipt)
  end

  def edit
  end

  def create
    @receipt = Receipt.new(receipt_params)
    user_id = current_user.id

    @receipt.articles.each do |article|
      warranty_time = article.warranty_time
      article.warranty_expires = @receipt.shopping_date.advance(months: warranty_time)
    end

    @receipt.user_id = user_id
    @receipt.save
    respond_with(@receipt)
  end

  def update
    if @receipt.update(receipt_params)
      redirect_to @receipt, notice: "Successfully updated receipt."
    else
      render :edit
    end
  end

  def destroy
    @receipt.destroy
    respond_with(@receipt)
  end

  private

  def set_shop
    @receipt = Receipt.find(params[:id])
  end

  def receipt_params
    params.require(:receipt).permit(:name, :shopping_date, :shop_id, :file, 
    articles_attributes: [:id, :name, :brand, :warranty_time, :warranty_expires, 
                          :receipt_id,  :_destroy])
  end
end

Ето как изглежда моят receipts.js.coffee:

jQuery ->
  $('#receipt_shopping_date').datepicker(dateFormat: 'yy-mm-dd')
  $.datepicker.setDefaults($.datepicker.regional['PL']);


  $('form').on 'click', '.remove_fields', (event) ->
  $(this).prev('input[type=hidden]').val('1')
  $(this).closest('fieldset').hide()
  event.preventDefault()

  $('form').on 'click', '.add_fields', (event) ->
  time = new Date().getTime()
  regexp = new RegExp($(this).data('id'), 'g')
  $(this).before($(this).data('fields').replace(regexp, time))
  event.preventDefault()


$(document).ready(jQuery)
$(document).on('page:load', jQuery)

И накрая ето моето виждане за добавяне на нова разписка и добавяне на статии към нея:

(other fields...)

<div class="large-12 columns">
<p>Add articles on the receipt:</p>
</div>

<div class="field">
  <div class="large-12 columns">


  <%= f.fields_for :articles do |builder| %>
        <div class="article_fields">
    <%= render "article_fields", :f => builder %>
        </div>
        <% end %>

    <%= link_to_add_fields "Add another article", f, :articles %>

  </div>
</div>


<div class="actions">
<div class="large-12 columns">
    <%= f.submit "Sumbit Receipt" %>
</div>
</div>


<% end %>

Както можете да видите, използвам помощен метод link_to_add_fields, ето как изглежда:

def link_to_add_fields(name, f, association)
new_object = f.object.send(association).klass.new
id = new_object.object_id
fields = f.fields_for(association, new_object, child_index: id) do |builder|
  render(association.to_s.singularize + "_fields", f: builder)
end
link_to(name, '#', class: "add_fields small button", data: {id: id, fields: fields.gsub("\n", "")}) 
end

И накрая, както можете да видите, генерирам частично, наречено _article_fields.html.erb, ето как изглежда:

<fieldset style="width:1400px">
<legend>new article</legend>

<div class="large-2 columns">
<%= f.text_field :name%>
</div>

<div class="large-2 columns">
<%= f.text_field :brand%>
</div>

<div class="large-2 columns">
<%= f.text_field :warranty_time, class: "warranty" %>
</div>

<div class="large-12 columns">
<%= link_to "delete article", '#', class: "remove_fields button small alert" %>
</div>

</fieldset>

А сега да се заемем с моя проблем. Когато създавам разписка за първи път, всичко е наред - виждам броя на артикулите в разписка в моя изглед на шоуто и warranty_expires във всяка статия.

Нещата се объркват, когато актуализирам или изтривам article_fields чрез разписки/редактиране:

1) Когато редактирам разписка и искам да премахна някоя от статиите (въпреки че визуално в моя изглед за редактиране те изчезват - JS изглежда работи), полетата не се премахват от моята БД, като по този начин изгледът за показване остава точно същият като беше преди.

Прост пример:

преди редакция: разписката ми има 6 артикула

по време на редактиране: натисна 3 пъти бутона „изтриване на статия“, така че разписката трябва да има 3 статии

след редакция: разписката все още има 6 статии

2) Когато редактирам касова бележка и искам да добавя поле за друга статия, стойността warranty_expires винаги е нула - как мога да я накарам да работи с действието за актуализиране в моя контролер за касови бележки? Опитах да използвам същия код като в моето действие за създаване:

@receipt.articles.each do |article|
warranty_time = article.warranty_time
article.warranty_expires = @receipt.shopping_date.advance(months: warranty_time)
end

но няма да работи. Някаква идея защо?

Прост пример:

Разписката вече има 2 артикула. Когато добавя третия, получавам следния резултат:

3 артикула - всички имат имена и полета warranty_time, но само 2 от тях имат стойност warranty_expires.

Цялата ви помощ ще бъде високо оценена. Благодаря ви предварително.


person Alexander    schedule 09.01.2015    source източник


Отговори (4)


Мисля, че можете да използвате някои обратни извиквания във вашия модел на статия за решаване на втория ви проблем,

Започнете да изтривате това, опитайте се да поддържате вашия контролер възможно най-опростен и да управлявате операциите във вашите модели.

 @receipt.articles.each do |article|
  warranty_time = article.warranty_time
  article.warranty_expires = @receipt.shopping_date.advance(months: warranty_time)
end

Във вашия модел на статия добавете няколко обратни извиквания

class Article < ActiveRecord::Base
  belongs_to :receipt

  def name_with_brand
    "#{name} #{brand}"
  end

  before_update :set_warranty_expires
  before_create :set_warranty_expires

  def set_warranty_expires
    self.warranty_expires = self.receipt.shopping_date.advance(months: self.warranty_time)
  end

end

Кодът не е тестван, но това е идеята. Дано помогне.

Проверете тези две скъпоценни камъни simple_form и nested_form това помага много при писане на големи формуляри и те работят добре един с друг.

person Jorge Najera T    schedule 09.01.2015
comment
Благодаря ти :). Решението по-горе проработи. Така че като цяло кодът на контролера трябва да бъде толкова прост, колкото може и по-сложни методи трябва да бъдат внедрени в моделите? Със сигурност ще го запомня в бъдеще :). - person Alexander; 09.01.2015
comment
Разбрахте правилно. Лесен начин за запомняне са дебелите модели и слабите контролери. - person Jorge Najera T; 10.01.2015

Актуализация: Успях да коригирам първия проблем.

Решението за първото решение е следното:

скритото поле :_destroy липсваше за премахване на статии.

Така че трябваше да променя следния код:

<div class="large-12 columns">
<%= link_to "delete article", '#', class: "remove_fields button small alert" %>
</div>

to:

<div class="large-12 columns">
<%= f.hidden_field :_destroy %>
<%= link_to "delete article", '#', class: "remove_fields button small alert" %>
</div>

Все още нямам представа как да поправя втория проблем.

person Alexander    schedule 09.01.2015

Първо забелязах, че имате цикъл във вашето ново действие reciepts_controller

2.times do 
  @receipt.articles.build 
end

Това означава, че статията ще бъде създадена само 2 пъти за тази рецепта.

По-добре премахнете цикъла, така че да можете да добавите толкова артикули, колкото искате. За проблем номер две добавете ред по-долу, за да редактирате действие към вашия контролер

@receipt.articles.build

Предполагам, че това би ти помогнало.

Освен това nested_form е чудесен скъпоценен камък за управление на този вид задачи.

 https://github.com/ryanb/nested_form

Виж това.

person monsur    schedule 09.01.2015

Това е проблем, при който .hide() се извиква в receipts.js.coffee. Най-лесният начин да поправя това, за който се сещам, е просто да заменя .hide() с .remove()

person Charles Williams    schedule 29.04.2018