models: fix touch not propagating when using nested attributes

Sometimes, when using nested attributes, touch doesn’t propagate to
parent relationships. (see https://github.com/rails/rails/issues/26726)

Specifically, this happens in our app when updating a dossier with
only new attachements (but without changing the value of any fields).

To work around this, we need to define the parent relationship
explicitely. This is good practice anyway.

Fix #3906
This commit is contained in:
Pierre de La Morinerie 2019-05-29 16:28:27 +00:00
parent 52d672486e
commit 51aacabf13
7 changed files with 57 additions and 12 deletions

View file

@ -1,7 +1,7 @@
class Avis < ApplicationRecord
include EmailSanitizableConcern
belongs_to :dossier, touch: true
belongs_to :dossier, inverse_of: :avis, touch: true
belongs_to :gestionnaire
belongs_to :claimant, class_name: 'Gestionnaire'

View file

@ -1,5 +1,5 @@
class Champ < ApplicationRecord
belongs_to :dossier, touch: true
belongs_to :dossier, inverse_of: :champs, touch: true
belongs_to :type_de_champ, inverse_of: :champ
belongs_to :parent, class_name: 'Champ'
has_many :commentaires

View file

@ -1,5 +1,5 @@
class Commentaire < ApplicationRecord
belongs_to :dossier, touch: true
belongs_to :dossier, inverse_of: :commentaires, touch: true
belongs_to :piece_justificative
belongs_to :user

View file

@ -19,18 +19,18 @@ class Dossier < ApplicationRecord
has_one :individual, dependent: :destroy
has_one :attestation, dependent: :destroy
has_many :pieces_justificatives, dependent: :destroy
has_many :pieces_justificatives, inverse_of: :dossier, dependent: :destroy
has_one_attached :justificatif_motivation
has_many :champs, -> { root.public_only.ordered }, dependent: :destroy
has_many :champs_private, -> { root.private_only.ordered }, class_name: 'Champ', dependent: :destroy
has_many :commentaires, dependent: :destroy
has_many :champs, -> { root.public_only.ordered }, inverse_of: :dossier, dependent: :destroy
has_many :champs_private, -> { root.private_only.ordered }, class_name: 'Champ', inverse_of: :dossier, dependent: :destroy
has_many :commentaires, inverse_of: :dossier, dependent: :destroy
has_many :invites, dependent: :destroy
has_many :follows, -> { active }
has_many :previous_follows, -> { inactive }, class_name: 'Follow'
has_many :followers_gestionnaires, through: :follows, source: :gestionnaire
has_many :previous_followers_gestionnaires, -> { distinct }, through: :previous_follows, source: :gestionnaire
has_many :avis, dependent: :destroy
has_many :avis, inverse_of: :dossier, dependent: :destroy
has_many :dossier_operation_logs, dependent: :destroy

View file

@ -1,5 +1,5 @@
class PieceJustificative < ApplicationRecord
belongs_to :dossier, touch: true
belongs_to :dossier, inverse_of: :pieces_justificatives, touch: true
belongs_to :type_de_piece_justificative
has_one :commentaire

View file

@ -386,9 +386,11 @@ describe Users::DossiersController, type: :controller do
describe '#update_brouillon' do
before { sign_in(user) }
let!(:dossier) { create(:dossier, user: user) }
let(:first_champ) { dossier.champs.first }
let(:value) { 'beautiful value' }
let(:now) { Time.zone.parse('01/01/2100') }
let(:submit_payload) do
{
id: dossier.id,
@ -402,7 +404,11 @@ describe Users::DossiersController, type: :controller do
end
let(:payload) { submit_payload }
subject { patch :update_brouillon, params: payload }
subject do
Timecop.freeze(now) do
patch :update_brouillon, params: payload
end
end
context 'when the dossier cannot be updated by the user' do
let!(:dossier) { create(:dossier, :en_instruction, user: user) }
@ -421,6 +427,7 @@ describe Users::DossiersController, type: :controller do
expect(response).to redirect_to(merci_dossier_path(dossier))
expect(first_champ.reload.value).to eq('beautiful value')
expect(dossier.reload.updated_at.year).to eq(2100)
expect(dossier.reload.state).to eq(Dossier.states.fetch(:en_construction))
end
@ -549,9 +556,15 @@ describe Users::DossiersController, type: :controller do
describe '#update' do
before { sign_in(user) }
let!(:dossier) { create(:dossier, :en_construction, user: user) }
let(:procedure) { create(:procedure, :published, :with_type_de_champ, :with_piece_justificative) }
let!(:dossier) { create(:dossier, :en_construction, user: user, procedure: procedure) }
let(:first_champ) { dossier.champs.first }
let(:piece_justificative_champ) { dossier.champs.last }
let(:value) { 'beautiful value' }
let(:file) { Rack::Test::UploadedFile.new("./spec/fixtures/files/piece_justificative_0.pdf", 'application/pdf') }
let(:now) { Time.zone.parse('01/01/2100') }
let(:submit_payload) do
{
id: dossier.id,
@ -565,7 +578,11 @@ describe Users::DossiersController, type: :controller do
end
let(:payload) { submit_payload }
subject { patch :update, params: payload }
subject do
Timecop.freeze(now) do
patch :update, params: payload
end
end
context 'when the dossier cannot be updated by the user' do
let!(:dossier) { create(:dossier, :en_instruction, user: user) }
@ -584,8 +601,28 @@ describe Users::DossiersController, type: :controller do
expect(response).to redirect_to(demande_dossier_path(dossier))
expect(first_champ.reload.value).to eq('beautiful value')
expect(dossier.reload.updated_at.year).to eq(2100)
expect(dossier.reload.state).to eq(Dossier.states.fetch(:en_construction))
end
context 'when only files champs are modified' do
let(:submit_payload) do
{
id: dossier.id,
dossier: {
champs_attributes: {
id: piece_justificative_champ.id,
piece_justificative_file: file
}
}
}
end
it 'updates the dossier modification date' do
subject
expect(dossier.reload.updated_at.year).to eq(2100)
end
end
end
context 'when the update fails' do

View file

@ -127,6 +127,14 @@ FactoryBot.define do
end
end
trait :with_piece_justificative do
after(:build) do |procedure, _evaluator|
type_de_champ = create(:type_de_champ_piece_justificative)
procedure.types_de_champ << type_de_champ
end
end
# Deprecated
trait :with_two_type_de_piece_justificative do
after(:build) do |procedure, _evaluator|
rib = create(:type_de_piece_justificative, :rib, order_place: 1)