From b8978d21964eb101598ba0e2c572cea4e0e8887f Mon Sep 17 00:00:00 2001 From: Pierre de La Morinerie Date: Wed, 6 Feb 2019 10:33:14 +0100 Subject: [PATCH 01/11] carto: properly handle RestClient::ServiceUnavailable exceptions --- app/lib/api_carto/api.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/lib/api_carto/api.rb b/app/lib/api_carto/api.rb index 84bcd5d70..f94c11e2d 100644 --- a/app/lib/api_carto/api.rb +++ b/app/lib/api_carto/api.rb @@ -15,7 +15,7 @@ class ApiCarto::API params = geojson.to_s RestClient.post(url, params, content_type: 'application/json') - rescue RestClient::InternalServerError, RestClient::BadGateway, RestClient::GatewayTimeout => e + rescue RestClient::InternalServerError, RestClient::BadGateway, RestClient::GatewayTimeout, RestClient::ServiceUnavailable => e Rails.logger.error "[ApiCarto] Error on #{url}: #{e}" raise RestClient::ResourceNotFound end From d251ebc2f422114d58b5e852f8894b6e19bf9cc9 Mon Sep 17 00:00:00 2001 From: Pierre de La Morinerie Date: Wed, 6 Feb 2019 18:20:35 +0100 Subject: [PATCH 02/11] dossiers: shorten method name --- app/controllers/new_user/dossiers_controller.rb | 2 +- app/models/dossier.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/controllers/new_user/dossiers_controller.rb b/app/controllers/new_user/dossiers_controller.rb index b848ff8ec..863ee7332 100644 --- a/app/controllers/new_user/dossiers_controller.rb +++ b/app/controllers/new_user/dossiers_controller.rb @@ -257,7 +257,7 @@ module NewUser end def ensure_dossier_can_be_updated - if !dossier.can_be_updated_by_the_user? + if !dossier.can_be_updated_by_user? flash.alert = 'Votre dossier ne peut plus être modifié' redirect_to dossiers_path end diff --git a/app/models/dossier.rb b/app/models/dossier.rb index f35370ed4..05d32436a 100644 --- a/app/models/dossier.rb +++ b/app/models/dossier.rb @@ -166,7 +166,7 @@ class Dossier < ApplicationRecord !procedure.archivee? && brouillon? end - def can_be_updated_by_the_user? + def can_be_updated_by_user? brouillon? || en_construction? end From a6704c4cd613180b9548bd8ec5a73093a0209cdd Mon Sep 17 00:00:00 2001 From: Pierre de La Morinerie Date: Wed, 6 Feb 2019 18:11:55 +0000 Subject: [PATCH 03/11] dossiers: allow users to delete "en construction" dossiers --- app/controllers/new_user/dossiers_controller.rb | 2 +- app/models/dossier.rb | 4 ++++ app/views/new_user/dossiers/index.html.haml | 2 +- spec/features/new_user/list_dossiers_spec.rb | 2 +- 4 files changed, 7 insertions(+), 3 deletions(-) diff --git a/app/controllers/new_user/dossiers_controller.rb b/app/controllers/new_user/dossiers_controller.rb index 863ee7332..f6871c8f5 100644 --- a/app/controllers/new_user/dossiers_controller.rb +++ b/app/controllers/new_user/dossiers_controller.rb @@ -188,7 +188,7 @@ module NewUser def ask_deletion dossier = current_user.dossiers.includes(:user, procedure: :administrateur).find(params[:id]) - if !dossier.instruction_commencee? + if dossier.can_be_deleted_by_user? dossier.delete_and_keep_track flash.notice = 'Votre dossier a bien été supprimé.' redirect_to dossiers_path diff --git a/app/models/dossier.rb b/app/models/dossier.rb index 05d32436a..c2da27547 100644 --- a/app/models/dossier.rb +++ b/app/models/dossier.rb @@ -170,6 +170,10 @@ class Dossier < ApplicationRecord brouillon? || en_construction? end + def can_be_deleted_by_user? + brouillon? || en_construction? + end + def retention_end_date if instruction_commencee? en_instruction_at + procedure.duree_conservation_dossiers_dans_ds.months diff --git a/app/views/new_user/dossiers/index.html.haml b/app/views/new_user/dossiers/index.html.haml index 0c83b7eeb..b9469c72c 100644 --- a/app/views/new_user/dossiers/index.html.haml +++ b/app/views/new_user/dossiers/index.html.haml @@ -51,7 +51,7 @@ = link_to(url_for_dossier(dossier), class: 'cell-link') do = dossier.updated_at.strftime("%d/%m/%Y") %td.action-col.delete-col - - if dossier.brouillon? + - if dossier.can_be_deleted_by_user? = link_to(ask_deletion_dossier_path(dossier), method: :post, class: 'button danger', data: { disable: true, confirm: "En continuant, vous allez supprimer ce dossier ainsi que les informations qu’il contient. Toute suppression entraine l’annulation de la démarche en cours.\n\nConfirmer la suppression ?" }) do %span.icon.delete Supprimer diff --git a/spec/features/new_user/list_dossiers_spec.rb b/spec/features/new_user/list_dossiers_spec.rb index f47133bac..319741ddd 100644 --- a/spec/features/new_user/list_dossiers_spec.rb +++ b/spec/features/new_user/list_dossiers_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' describe 'user access to the list of his dossier' do let(:user) { create(:user) } let!(:last_updated_dossier) { create(:dossier, :with_entreprise, user: user, state: Dossier.states.fetch(:en_construction)) } - let!(:dossier1) { create(:dossier, :with_entreprise, user: user, state: Dossier.states.fetch(:en_construction)) } + let!(:dossier1) { create(:dossier, :with_entreprise, user: user, state: Dossier.states.fetch(:en_instruction)) } let!(:dossier2) { create(:dossier, :with_entreprise) } let!(:dossier_brouillon) { create(:dossier, :with_entreprise, user: user) } let!(:dossier_archived) { create(:dossier, :with_entreprise, user: user, state: Dossier.states.fetch(:en_construction)) } From 4bfc258cbd153e2a5a9eff18085ede78a9aff2a8 Mon Sep 17 00:00:00 2001 From: Pierre de La Morinerie Date: Wed, 6 Feb 2019 19:12:04 +0100 Subject: [PATCH 04/11] dossiers: refactor dossiers specs - Use login_as instead of signing up in the browser - Better name factory objects - Group specs by use-case --- .../new_user/dossiers_controller_spec.rb | 8 +- spec/features/new_user/list_dossiers_spec.rb | 103 +++++++++--------- 2 files changed, 58 insertions(+), 53 deletions(-) diff --git a/spec/controllers/new_user/dossiers_controller_spec.rb b/spec/controllers/new_user/dossiers_controller_spec.rb index bc08ba78b..198934abb 100644 --- a/spec/controllers/new_user/dossiers_controller_spec.rb +++ b/spec/controllers/new_user/dossiers_controller_spec.rb @@ -798,13 +798,13 @@ describe NewUser::DossiersController, type: :controller do subject { post :ask_deletion, params: { id: dossier.id } } shared_examples_for "the dossier can not be deleted" do - it do + it "doesn’t notify the deletion" do expect(DossierMailer).not_to receive(:notify_deletion_to_administration) expect(DossierMailer).not_to receive(:notify_deletion_to_user) subject end - it do + it "doesn’t delete the dossier" do subject expect(Dossier.find_by(id: dossier.id)).not_to eq(nil) expect(dossier.procedure.deleted_dossiers.count).to eq(0) @@ -814,13 +814,13 @@ describe NewUser::DossiersController, type: :controller do context 'when dossier is owned by signed in user' do let(:dossier) { create(:dossier, :en_construction, user: user, autorisation_donnees: true) } - it do + it "notifies the user and the admin of the deletion" do expect(DossierMailer).to receive(:notify_deletion_to_administration).with(kind_of(DeletedDossier), dossier.procedure.administrateur.email).and_return(double(deliver_later: nil)) expect(DossierMailer).to receive(:notify_deletion_to_user).with(kind_of(DeletedDossier), dossier.user.email).and_return(double(deliver_later: nil)) subject end - it do + it "deletes the dossier" do procedure = dossier.procedure dossier_id = dossier.id subject diff --git a/spec/features/new_user/list_dossiers_spec.rb b/spec/features/new_user/list_dossiers_spec.rb index 319741ddd..ac0aeb80c 100644 --- a/spec/features/new_user/list_dossiers_spec.rb +++ b/spec/features/new_user/list_dossiers_spec.rb @@ -1,13 +1,13 @@ require 'spec_helper' -describe 'user access to the list of his dossier' do +describe 'user access to the list of their dossiers' do let(:user) { create(:user) } - let!(:last_updated_dossier) { create(:dossier, :with_entreprise, user: user, state: Dossier.states.fetch(:en_construction)) } - let!(:dossier1) { create(:dossier, :with_entreprise, user: user, state: Dossier.states.fetch(:en_instruction)) } - let!(:dossier2) { create(:dossier, :with_entreprise) } - let!(:dossier_brouillon) { create(:dossier, :with_entreprise, user: user) } - let!(:dossier_archived) { create(:dossier, :with_entreprise, user: user, state: Dossier.states.fetch(:en_construction)) } + let!(:dossier_brouillon) { create(:dossier, user: user) } + let!(:dossier_en_construction) { create(:dossier, :en_construction, user: user) } + let!(:dossier_en_instruction) { create(:dossier, :en_instruction, user: user) } + let!(:dossier_archived) { create(:dossier, :en_instruction, :archived, user: user) } let(:dossiers_per_page) { 25 } + let(:last_updated_dossier) { dossier_en_construction } before do @default_per_page = Dossier.default_per_page @@ -15,12 +15,8 @@ describe 'user access to the list of his dossier' do last_updated_dossier.update_column(:updated_at, "19/07/2052 15:35".to_time) - visit new_user_session_path - within('#new_user') do - page.find_by_id('user_email').set user.email - page.find_by_id('user_password').set user.password - page.click_on 'Se connecter' - end + login_as user, scope: :user + visit dossiers_path end after do @@ -28,52 +24,59 @@ describe 'user access to the list of his dossier' do end it 'the list of dossier is displayed' do - expect(page).to have_content(dossier1.procedure.libelle) - expect(page).to have_content('en construction') - end - - it 'dossiers belonging to other users are not displayed' do - expect(page).not_to have_content(dossier2.procedure.libelle) - end - - it 'the list must be ordered by last updated' do - expect(page.body).to match(/#{last_updated_dossier.procedure.libelle}.*#{dossier1.procedure.libelle}/m) - end - - it 'should list archived dossiers' do + expect(page).to have_content(dossier_brouillon.procedure.libelle) + expect(page).to have_content(dossier_en_construction.procedure.libelle) + expect(page).to have_content(dossier_en_instruction.procedure.libelle) expect(page).to have_content(dossier_archived.procedure.libelle) end - it 'should have link to only delete brouillon' do - expect(page).to have_link(nil, href: ask_deletion_dossier_path(dossier_brouillon)) - expect(page).not_to have_link(nil, href: ask_deletion_dossier_path(dossier1)) + it 'the list must be ordered by last updated' do + expect(page.body).to match(/#{last_updated_dossier.procedure.libelle}.*#{dossier_en_instruction.procedure.libelle}/m) end - context 'when user clicks on delete brouillon', js: true do - scenario 'dossier is deleted' do - page.accept_alert('Confirmer la suppression ?') do - find(:xpath, "//a[@href='#{ask_deletion_dossier_path(dossier_brouillon)}']").click - end + context 'when there are dossiers from other users' do + let!(:dossier_other_user) { create(:dossier) } - expect(page).to have_content('Votre dossier a bien été supprimé.') - end - end - - context 'when user clicks on a projet in list', js: true do - before do - page.click_on(dossier1.procedure.libelle) - end - scenario 'user is redirected to dossier page' do - expect(page).to have_current_path(dossier_path(dossier1)) + it 'doesn’t display dossiers belonging to other users' do + expect(page).not_to have_content(dossier_other_user.procedure.libelle) end end context 'when there is more than one page' do let(:dossiers_per_page) { 2 } - scenario 'the user can navigate through the other pages', js: true do + scenario 'the user can navigate through the other pages' do + expect(page).not_to have_content(dossier_en_instruction.procedure.libelle) page.click_link("Suivant") - expect(page).to have_content(dossier_archived.procedure.libelle) + expect(page).to have_content(dossier_en_instruction.procedure.libelle) + end + end + + context 'when user clicks on a projet in list' do + before do + page.click_on(dossier_en_construction.procedure.libelle) + end + + scenario 'user is redirected to dossier page' do + expect(page).to have_current_path(dossier_path(dossier_en_construction)) + end + end + + describe 'deletion' do + it 'should have links to delete dossiers' do + expect(page).to have_link(nil, href: ask_deletion_dossier_path(dossier_brouillon)) + expect(page).to have_link(nil, href: ask_deletion_dossier_path(dossier_en_construction)) + expect(page).not_to have_link(nil, href: ask_deletion_dossier_path(dossier_en_instruction)) + end + + context 'when user clicks on delete button', js: true do + scenario 'the dossier is deleted' do + page.accept_alert('Confirmer la suppression ?') do + find(:xpath, "//a[@href='#{ask_deletion_dossier_path(dossier_brouillon)}']").click + end + + expect(page).to have_content('Votre dossier a bien été supprimé.') + end end end @@ -91,25 +94,27 @@ describe 'user access to the list of his dossier' do end context "when the dossier does not belong to the user" do + let!(:dossier_other_user) { create(:dossier) } + before do - page.find_by_id('dossier_id').set(dossier2.id) + page.find_by_id('dossier_id').set(dossier_other_user.id) click_button("Rechercher") end it "shows an error message on the dossiers page" do expect(current_path).to eq(dossiers_path) - expect(page).to have_content("Vous n’avez pas de dossier avec le nº #{dossier2.id}.") + expect(page).to have_content("Vous n’avez pas de dossier avec le nº #{dossier_other_user.id}.") end end context "when the dossier belongs to the user" do before do - page.find_by_id('dossier_id').set(dossier1.id) + page.find_by_id('dossier_id').set(dossier_en_construction.id) click_button("Rechercher") end it "redirects to the dossier page" do - expect(current_path).to eq(dossier_path(dossier1)) + expect(current_path).to eq(dossier_path(dossier_en_construction)) end end end From a0a1ce11c89ace9a49e542a07516b46611c7e1fb Mon Sep 17 00:00:00 2001 From: Paul Chavard Date: Thu, 7 Feb 2019 10:44:15 +0100 Subject: [PATCH 05/11] Add repetition to apercu --- app/models/dossier.rb | 12 +++--------- app/models/procedure.rb | 11 ++++++++--- app/models/type_de_champ.rb | 4 ++++ .../types_de_champ/repetition_type_de_champ.rb | 5 +++++ app/models/types_de_champ/type_de_champ_base.rb | 4 ++++ .../dossiers/editable_champs/_repetition.html.haml | 8 +++----- 6 files changed, 27 insertions(+), 17 deletions(-) diff --git a/app/models/dossier.rb b/app/models/dossier.rb index c2da27547..70f023e26 100644 --- a/app/models/dossier.rb +++ b/app/models/dossier.rb @@ -120,17 +120,11 @@ class Dossier < ApplicationRecord end def build_default_champs - procedure.types_de_champ.each do |type_de_champ| - champ = type_de_champ.champ.build - - if type_de_champ.repetition? - champ.add_row - end - + procedure.build_champs.each do |champ| champs << champ end - procedure.types_de_champ_private.each do |type_de_champ| - champs_private << type_de_champ.champ.build + procedure.build_champs_private.each do |champ| + champs_private << champ end end diff --git a/app/models/procedure.rb b/app/models/procedure.rb index 7fd28e5ed..c48b3ff37 100644 --- a/app/models/procedure.rb +++ b/app/models/procedure.rb @@ -138,10 +138,15 @@ class Procedure < ApplicationRecord # Warning: dossier after_save build_default_champs must be removed # to save a dossier created from this method def new_dossier - champs = types_de_champ.map { |tdc| tdc.champ.build } - champs_private = types_de_champ_private.map { |tdc| tdc.champ.build } + Dossier.new(procedure: self, champs: build_champs, champs_private: build_champs_private) + end - Dossier.new(procedure: self, champs: champs, champs_private: champs_private) + def build_champs + types_de_champ.map(&:build_champ) + end + + def build_champs_private + types_de_champ_private.map(&:build_champ) end def default_path diff --git a/app/models/type_de_champ.rb b/app/models/type_de_champ.rb index 727767c6e..99d38e88e 100644 --- a/app/models/type_de_champ.rb +++ b/app/models/type_de_champ.rb @@ -120,6 +120,10 @@ class TypeDeChamp < ApplicationRecord } end + def build_champ + dynamic_type.build_champ + end + def self.type_de_champs_list_fr type_champs.map { |champ| [I18n.t("activerecord.attributes.type_de_champ.type_champs.#{champ.last}"), champ.first] } end diff --git a/app/models/types_de_champ/repetition_type_de_champ.rb b/app/models/types_de_champ/repetition_type_de_champ.rb index 5abf8efd3..c3328850a 100644 --- a/app/models/types_de_champ/repetition_type_de_champ.rb +++ b/app/models/types_de_champ/repetition_type_de_champ.rb @@ -1,2 +1,7 @@ class TypesDeChamp::RepetitionTypeDeChamp < TypesDeChamp::TypeDeChampBase + def build_champ + champ = super + champ.add_row + champ + end end diff --git a/app/models/types_de_champ/type_de_champ_base.rb b/app/models/types_de_champ/type_de_champ_base.rb index fb05bd2ab..410663aa3 100644 --- a/app/models/types_de_champ/type_de_champ_base.rb +++ b/app/models/types_de_champ/type_de_champ_base.rb @@ -19,4 +19,8 @@ class TypesDeChamp::TypeDeChampBase } ] end + + def build_champ + @type_de_champ.champ.build + end end diff --git a/app/views/shared/dossiers/editable_champs/_repetition.html.haml b/app/views/shared/dossiers/editable_champs/_repetition.html.haml index d7e8717c7..ff5f35644 100644 --- a/app/views/shared/dossiers/editable_champs/_repetition.html.haml +++ b/app/views/shared/dossiers/editable_champs/_repetition.html.haml @@ -5,11 +5,9 @@ = form.fields_for :champs, champ do |form| = render partial: 'shared/dossiers/editable_champs/editable_champ', locals: { champ: form.object, form: form } = form.hidden_field :_destroy, disabled: true - %button.button.danger.remove-row - Supprimer + - if champ.persisted? + %button.button.danger.remove-row + Supprimer - if champ.persisted? = link_to "Ajouter une ligne pour « #{champ.libelle} »", champs_repetition_path(form.index), class: 'button add-row', data: { remote: true, method: 'POST', params: { champ_id: champ&.id }.to_query } -- else - %button.button.add-row{ disabled: true } - = "Ajouter une ligne pour « #{champ.libelle} »" From bb5c90c579b8ac79345c02c9d3316f20e350aaad Mon Sep 17 00:00:00 2001 From: Paul Chavard Date: Thu, 7 Feb 2019 11:01:21 +0100 Subject: [PATCH 06/11] Fix champ address on repetitions --- app/javascript/shared/autocomplete.js | 29 ++++++++++++++++++++------- 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/app/javascript/shared/autocomplete.js b/app/javascript/shared/autocomplete.js index f5cf5861c..985862d59 100644 --- a/app/javascript/shared/autocomplete.js +++ b/app/javascript/shared/autocomplete.js @@ -38,13 +38,28 @@ function source(url) { } addEventListener('turbolinks:load', function() { + autocompleteSetup(); +}); + +addEventListener('ajax:success', function() { + autocompleteSetup(); +}); + +function autocompleteSetup() { for (let { type, url } of sources) { - for (let target of document.querySelectorAll(selector(type))) { - let select = autocomplete(target, options, [source(url)]); - select.on('autocomplete:selected', ({ target }, suggestion) => { - fire(target, 'autocomplete:select', suggestion); - select.autocomplete.setVal(suggestion.label); - }); + for (let element of document.querySelectorAll(selector(type))) { + if (!element.dataset.autocompleteInitialized) { + autocompleteInitializeElement(element, url); + } } } -}); +} + +function autocompleteInitializeElement(element, url) { + const select = autocomplete(element, options, [source(url)]); + select.on('autocomplete:selected', ({ target }, suggestion) => { + fire(target, 'autocomplete:select', suggestion); + select.autocomplete.setVal(suggestion.label); + }); + element.dataset.autocompleteInitialized = true; +} From f5c9b55c59192e036c4add19b74194e976bcbb39 Mon Sep 17 00:00:00 2001 From: Paul Chavard Date: Thu, 7 Feb 2019 13:10:29 +0100 Subject: [PATCH 07/11] Move remove row button to the right and show button on previews --- app/assets/stylesheets/new_design/flex.scss | 4 ++++ .../dossiers/editable_champs/_repetition.html.haml | 13 ++++++++++--- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/app/assets/stylesheets/new_design/flex.scss b/app/assets/stylesheets/new_design/flex.scss index 317ebf55c..b16b0e243 100644 --- a/app/assets/stylesheets/new_design/flex.scss +++ b/app/assets/stylesheets/new_design/flex.scss @@ -32,6 +32,10 @@ &.column { flex-direction: column; } + + &.row-reverse { + flex-direction: row-reverse; + } } .flex-grow { diff --git a/app/views/shared/dossiers/editable_champs/_repetition.html.haml b/app/views/shared/dossiers/editable_champs/_repetition.html.haml index ff5f35644..7f25613da 100644 --- a/app/views/shared/dossiers/editable_champs/_repetition.html.haml +++ b/app/views/shared/dossiers/editable_champs/_repetition.html.haml @@ -5,9 +5,16 @@ = form.fields_for :champs, champ do |form| = render partial: 'shared/dossiers/editable_champs/editable_champ', locals: { champ: form.object, form: form } = form.hidden_field :_destroy, disabled: true - - if champ.persisted? - %button.button.danger.remove-row - Supprimer + .flex.row-reverse + - if champ.persisted? + %button.button.danger.remove-row + Supprimer + - else + %button.button.danger{ type: :button } + Supprimer - if champ.persisted? = link_to "Ajouter une ligne pour « #{champ.libelle} »", champs_repetition_path(form.index), class: 'button add-row', data: { remote: true, method: 'POST', params: { champ_id: champ&.id }.to_query } +- else + %button.button{ type: :button } + = "Ajouter une ligne pour « #{champ.libelle} »" From 82fc01743024ab30c555fe826c919731ad19f89a Mon Sep 17 00:00:00 2001 From: Pierre de La Morinerie Date: Thu, 7 Feb 2019 12:16:33 +0000 Subject: [PATCH 08/11] autocomplete: fix initialization happening several times --- app/javascript/shared/autocomplete.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/app/javascript/shared/autocomplete.js b/app/javascript/shared/autocomplete.js index 985862d59..db9db0180 100644 --- a/app/javascript/shared/autocomplete.js +++ b/app/javascript/shared/autocomplete.js @@ -48,9 +48,8 @@ addEventListener('ajax:success', function() { function autocompleteSetup() { for (let { type, url } of sources) { for (let element of document.querySelectorAll(selector(type))) { - if (!element.dataset.autocompleteInitialized) { - autocompleteInitializeElement(element, url); - } + element.removeAttribute('data-autocomplete'); + autocompleteInitializeElement(element, url); } } } @@ -61,5 +60,4 @@ function autocompleteInitializeElement(element, url) { fire(target, 'autocomplete:select', suggestion); select.autocomplete.setVal(suggestion.label); }); - element.dataset.autocompleteInitialized = true; } From 5da5f75c5f74f783db7a5e77933c72320a23bbaf Mon Sep 17 00:00:00 2001 From: Paul Chavard Date: Wed, 6 Feb 2019 18:19:27 +0100 Subject: [PATCH 09/11] [Types de Champ Editeur] Save on change and only edited model --- .../procedures_controller.rb | 47 +--- .../types_de_champ_controller.rb | 76 +++++++ app/helpers/procedure_helper.rb | 19 +- .../administrateur/DraggableItem.js | 206 +++++++++++++----- .../administrateur/DraggableItem.vue | 42 ++-- .../administrateur/DraggableList.js | 9 +- .../administrateur/DraggableList.vue | 5 +- .../administrateur/champs-editor.js | 124 ++++------- app/models/type_de_champ.rb | 8 + .../procedures/update.js.erb | 3 - config/routes.rb | 2 + .../features/admin/procedure_creation_spec.rb | 6 +- ...cedures_spec.rb => types_de_champ_spec.rb} | 54 ++--- spec/support/feature_helpers.rb | 4 + 14 files changed, 351 insertions(+), 254 deletions(-) create mode 100644 app/controllers/new_administrateur/types_de_champ_controller.rb delete mode 100644 app/views/new_administrateur/procedures/update.js.erb rename spec/features/new_administrateur/{procedures_spec.rb => types_de_champ_spec.rb} (67%) diff --git a/app/controllers/new_administrateur/procedures_controller.rb b/app/controllers/new_administrateur/procedures_controller.rb index ff4734397..e51e75aa0 100644 --- a/app/controllers/new_administrateur/procedures_controller.rb +++ b/app/controllers/new_administrateur/procedures_controller.rb @@ -1,49 +1,13 @@ module NewAdministrateur class ProceduresController < AdministrateurController - before_action :retrieve_procedure, only: [:champs, :annotations, :update] - before_action :procedure_locked?, only: [:champs, :annotations, :update] - - TYPE_DE_CHAMP_ATTRIBUTES_BASE = [ - :_destroy, - :libelle, - :description, - :order_place, - :type_champ, - :id, - :mandatory, - :piece_justificative_template, - :quartiers_prioritaires, - :cadastres, - :parcelles_agricoles, - drop_down_list_attributes: [:value] - ] - - TYPE_DE_CHAMP_ATTRIBUTES = TYPE_DE_CHAMP_ATTRIBUTES_BASE.dup - TYPE_DE_CHAMP_ATTRIBUTES << { - types_de_champ_attributes: TYPE_DE_CHAMP_ATTRIBUTES_BASE - } + before_action :retrieve_procedure, only: [:champs, :annotations] + before_action :procedure_locked?, only: [:champs, :annotations] def apercu @dossier = procedure_without_control.new_dossier @tab = apercu_tab end - def update - if @procedure.update(procedure_params) - flash.now.notice = if params[:procedure][:types_de_champ_attributes].present? - 'Formulaire mis à jour.' - elsif params[:procedure][:types_de_champ_private_attributes].present? - 'Annotations privées mises à jour.' - else - 'Démarche enregistrée.' - end - - reset_procedure - else - flash.now.alert = @procedure.errors.full_messages - end - end - private def apercu_tab @@ -53,12 +17,5 @@ module NewAdministrateur def procedure_without_control Procedure.find(params[:id]) end - - def procedure_params - params.required(:procedure).permit( - types_de_champ_attributes: TYPE_DE_CHAMP_ATTRIBUTES, - types_de_champ_private_attributes: TYPE_DE_CHAMP_ATTRIBUTES - ) - end end end diff --git a/app/controllers/new_administrateur/types_de_champ_controller.rb b/app/controllers/new_administrateur/types_de_champ_controller.rb new file mode 100644 index 000000000..c4bf50110 --- /dev/null +++ b/app/controllers/new_administrateur/types_de_champ_controller.rb @@ -0,0 +1,76 @@ +module NewAdministrateur + class TypesDeChampController < AdministrateurController + before_action :retrieve_procedure, only: [:create, :update, :destroy] + before_action :procedure_locked?, only: [:create, :update, :destroy] + + def create + type_de_champ = TypeDeChamp.new(type_de_champ_create_params) + + if type_de_champ.save + reset_procedure + render json: serialize_type_de_champ(type_de_champ), status: :created + else + render json: { errors: type_de_champ.errors.full_messages }, status: :unprocessable_entity + end + end + + def update + type_de_champ = TypeDeChamp.where(procedure: @procedure).find(params[:id]) + + if type_de_champ.update(type_de_champ_update_params) + reset_procedure + render json: serialize_type_de_champ(type_de_champ) + else + render json: { errors: type_de_champ.errors.full_messages }, status: :unprocessable_entity + end + end + + def destroy + type_de_champ = TypeDeChamp.where(procedure: @procedure).find(params[:id]) + + type_de_champ.destroy! + reset_procedure + + head :no_content + end + + private + + def serialize_type_de_champ(type_de_champ) + { + type_de_champ: type_de_champ.as_json( + except: [:created_at, :updated_at, :stable_id, :type, :parent_id, :procedure_id, :private], + methods: [:piece_justificative_template_filename, :piece_justificative_template_url, :drop_down_list_value] + ) + } + end + + def type_de_champ_create_params + params.required(:type_de_champ).permit(:libelle, + :description, + :order_place, + :type_champ, + :private, + :parent_id, + :mandatory, + :piece_justificative_template, + :quartiers_prioritaires, + :cadastres, + :parcelles_agricoles, + :drop_down_list_value).merge(procedure: @procedure) + end + + def type_de_champ_update_params + params.required(:type_de_champ).permit(:libelle, + :description, + :order_place, + :type_champ, + :mandatory, + :piece_justificative_template, + :quartiers_prioritaires, + :cadastres, + :parcelles_agricoles, + :drop_down_list_value) + end + end +end diff --git a/app/helpers/procedure_helper.rb b/app/helpers/procedure_helper.rb index fe3395201..3222f0064 100644 --- a/app/helpers/procedure_helper.rb +++ b/app/helpers/procedure_helper.rb @@ -39,7 +39,8 @@ module ProcedureHelper type: "champ", types_de_champ_options: types_de_champ_options.to_json, types_de_champ: types_de_champ_as_json(procedure.types_de_champ).to_json, - direct_uploads_url: rails_direct_uploads_url, + save_url: procedure_types_de_champ_path(procedure), + direct_upload_url: rails_direct_uploads_url, drag_icon_url: image_url("icons/drag.svg") } end @@ -49,18 +50,12 @@ module ProcedureHelper type: "annotation", types_de_champ_options: types_de_champ_options.to_json, types_de_champ: types_de_champ_as_json(procedure.types_de_champ_private).to_json, - direct_uploads_url: rails_direct_uploads_url, + save_url: procedure_types_de_champ_path(procedure), + direct_upload_url: rails_direct_uploads_url, drag_icon_url: image_url("icons/drag.svg") } end - def procedure_data(procedure) - { - types_de_champ: types_de_champ_as_json(procedure.types_de_champ), - types_de_champ_private: types_de_champ_as_json(procedure.types_de_champ_private) - }.to_json - end - private TOGGLES = { @@ -79,14 +74,12 @@ module ProcedureHelper types_de_champ end - TYPES_DE_CHAMP_INCLUDE = { drop_down_list: { only: :value } } TYPES_DE_CHAMP_BASE = { except: [:created_at, :updated_at, :stable_id, :type, :parent_id, :procedure_id, :private], - methods: [:piece_justificative_template_filename, :piece_justificative_template_url], - include: TYPES_DE_CHAMP_INCLUDE + methods: [:piece_justificative_template_filename, :piece_justificative_template_url, :drop_down_list_value] } TYPES_DE_CHAMP = TYPES_DE_CHAMP_BASE - .merge(include: TYPES_DE_CHAMP_INCLUDE.merge(types_de_champ: TYPES_DE_CHAMP_BASE)) + .merge(include: { types_de_champ: TYPES_DE_CHAMP_BASE }) def types_de_champ_as_json(types_de_champ) types_de_champ.includes(:drop_down_list, diff --git a/app/javascript/new_design/administrateur/DraggableItem.js b/app/javascript/new_design/administrateur/DraggableItem.js index 95d284120..ea6c1361b 100644 --- a/app/javascript/new_design/administrateur/DraggableItem.js +++ b/app/javascript/new_design/administrateur/DraggableItem.js @@ -1,40 +1,23 @@ +import { getJSON, debounce } from '@utils'; +import { DirectUpload } from 'activestorage'; + export default { - props: ['state', 'update', 'index', 'item'], + props: ['state', 'index', 'item'], computed: { - isDirty() { - return ( - this.state.version && - this.state.unsavedInvalidItems.size > 0 && - this.state.unsavedItems.has(this.itemId) - ); - }, - isInvalid() { + isValid() { if (this.deleted) { - return false; + return true; } if (this.libelle) { - return !this.libelle.trim(); + return !!this.libelle.trim(); } - return true; - }, - itemId() { - return this.item.id || this.clientId; - }, - changeLog() { - return [this.itemId, !this.isInvalid]; + return false; }, itemClassName() { const classNames = [`draggable-item-${this.index}`]; if (this.isHeaderSection) { classNames.push('type-header-section'); } - if (this.isDirty) { - if (this.isInvalid) { - classNames.push('invalid'); - } else { - classNames.push('dirty'); - } - } return classNames.join(' '); }, isDropDown() { @@ -73,6 +56,47 @@ export default { return 'types_de_champ_attributes'; } }, + payload() { + const payload = { + libelle: this.libelle, + type_champ: this.typeChamp, + mandatory: this.mandatory, + description: this.description, + drop_down_list_value: this.dropDownListValue, + order_place: this.index + }; + if (this.pieceJustificativeTemplate) { + payload.piece_justificative_template = this.pieceJustificativeTemplate; + } + if (this.state.parentId) { + payload.parent_id = this.state.parentId; + } + if (!this.id && this.state.isAnnotation) { + payload.private = true; + } + Object.assign(payload, this.options); + return payload; + }, + saveUrl() { + if (this.id) { + return `${this.state.saveUrl}/${this.id}`; + } + return this.state.saveUrl; + }, + savePayload() { + if (this.deleted) { + return {}; + } + return { type_de_champ: this.payload }; + }, + saveMethod() { + if (this.deleted) { + return 'delete'; + } else if (this.id) { + return 'patch'; + } + return 'post'; + }, typesDeChamp() { return this.item.types_de_champ; }, @@ -85,39 +109,46 @@ export default { return Object.assign({}, this.state, { typesDeChamp: this.typesDeChamp, typesDeChampOptions: this.typesDeChampOptions, - prefix: `${this.state.prefix}[${this.attribute}][${this.index}]` + prefix: `${this.state.prefix}[${this.attribute}][${this.index}]`, + parentId: this.id }); } }, data() { return { + id: this.item.id, typeChamp: this.item.type_champ, libelle: this.item.libelle, mandatory: this.item.mandatory, description: this.item.description, - dropDownList: this.item.drop_down_list && this.item.drop_down_list.value, + pieceJustificativeTemplate: null, + pieceJustificativeTemplateUrl: this.item.piece_justificative_template_url, + pieceJustificativeTemplateFilename: this.item + .piece_justificative_template_filename, + dropDownListValue: this.item.drop_down_list_value, deleted: false, - clientId: `id-${clientIds++}` + isSaving: false, + isUploading: false, + hasChanges: false }; }, - created() { - for (let path of PATHS_TO_WATCH) { - this.$watch(path, () => this.update(this.changeLog)); + watch: { + index() { + this.update(); } }, - mounted() { - if (this.isInvalid) { - this.update(this.changeLog, false); - } + created() { + this.debouncedSave = debounce(() => this.save(), 500); + this.debouncedUpload = debounce(evt => this.upload(evt), 500); }, methods: { - removeChamp(item) { - if (item.id) { + removeChamp() { + if (this.id) { this.deleted = true; + this.debouncedSave(); } else { - const index = this.state.typesDeChamp.indexOf(item); + const index = this.state.typesDeChamp.indexOf(this.item); this.state.typesDeChamp.splice(index, 1); - this.update([this.itemId, true]); } }, nameFor(name) { @@ -132,6 +163,76 @@ export default { type_champ: 'text', types_de_champ: [] }); + }, + update() { + this.hasChanges = true; + if (this.isValid) { + if (this.state.inFlight === 0) { + this.state.flash.clear(); + } + this.debouncedSave(); + } + }, + upload(evt) { + if (this.isUploading) { + this.debouncedUpload(); + } else { + const input = evt.target; + const file = input.files[0]; + if (file) { + this.isUploading = true; + uploadFile(this.state.directUploadUrl, file).then(({ signed_id }) => { + this.pieceJustificativeTemplate = signed_id; + this.isUploading = false; + this.debouncedSave(); + }); + } + input.value = null; + } + }, + save() { + if (this.isSaving) { + this.debouncedSave(); + } else { + this.isSaving = true; + this.state.inFlight++; + getJSON(this.saveUrl, this.savePayload, this.saveMethod) + .then(data => { + this.onSuccess(data); + }) + .catch(xhr => { + this.onError(xhr); + }); + } + }, + onSuccess(data) { + if (data && data.type_de_champ) { + this.id = data.type_de_champ.id; + this.pieceJustificativeTemplateUrl = + data.type_de_champ.piece_justificative_template_url; + this.pieceJustificativeTemplateFilename = + data.type_de_champ.piece_justificative_template_filename; + this.pieceJustificativeTemplate = null; + } + this.state.inFlight--; + this.isSaving = false; + this.hasChanges = false; + + if (this.state.inFlight === 0) { + this.state.flash.success(); + } + }, + onError(xhr) { + this.isSaving = false; + this.state.inFlight--; + try { + const { + errors: [message] + } = JSON.parse(xhr.responseText); + this.state.flash.error(message); + } catch (e) { + this.state.flash.error(xhr.responseText); + } } } }; @@ -143,21 +244,20 @@ const EXCLUDE_FROM_REPETITION = [ 'siret' ]; -const PATHS_TO_WATCH = [ - 'typeChamp', - 'libelle', - 'mandatory', - 'description', - 'dropDownList', - 'options.quartiers_prioritaires', - 'options.cadastres', - 'options.parcelles_agricoles', - 'index', - 'deleted' -]; - function castBoolean(value) { return value && value != 0; } -let clientIds = 0; +function uploadFile(directUploadUrl, file) { + const upload = new DirectUpload(file, directUploadUrl); + + return new Promise((resolve, reject) => { + upload.create((error, blob) => { + if (error) { + reject(error); + } else { + resolve(blob); + } + }); + }); +} diff --git a/app/javascript/new_design/administrateur/DraggableItem.vue b/app/javascript/new_design/administrateur/DraggableItem.vue index fbb70a5c8..1351dc395 100644 --- a/app/javascript/new_design/administrateur/DraggableItem.vue +++ b/app/javascript/new_design/administrateur/DraggableItem.vue @@ -1,6 +1,6 @@