From 862ab4ed04bc695d91922eee70e296c0ff12b8fb Mon Sep 17 00:00:00 2001 From: Paul Chavard Date: Mon, 4 Feb 2019 15:46:11 +0100 Subject: [PATCH 1/3] =?UTF-8?q?=E2=80=9CBloc=20r=C3=A9p=C3=A9table?= =?UTF-8?q?=E2=80=9D=20is=20ready=20to=20be=20tested?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config/features.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/features.rb b/config/features.rb index 2faf85f48..55d02a7bc 100644 --- a/config/features.rb +++ b/config/features.rb @@ -10,7 +10,7 @@ Flipflop.configure do feature :champ_integer_number, title: "Champ nombre entier" feature :champ_repetition, - title: "Bloc répétable (NE MARCHE PAS – NE PAS ACTIVER)" + title: "Bloc répétable" end feature :web_hook From a4a421a91a2b49a3569b223c4a2f00ccb73f0d5e Mon Sep 17 00:00:00 2001 From: Paul Chavard Date: Wed, 30 Jan 2019 17:20:02 +0100 Subject: [PATCH 2/3] Champ Repetition dossier display --- app/assets/stylesheets/new_design/table.scss | 4 ++ app/models/type_de_champ.rb | 5 +-- .../shared/dossiers/_champ_row.html.haml | 40 +++++++++++++++++++ app/views/shared/dossiers/_champs.html.haml | 32 +-------------- .../shared/dossiers/_champs.html.haml_spec.rb | 2 +- 5 files changed, 47 insertions(+), 36 deletions(-) create mode 100644 app/views/shared/dossiers/_champ_row.html.haml diff --git a/app/assets/stylesheets/new_design/table.scss b/app/assets/stylesheets/new_design/table.scss index 28afbb340..1f461e9e4 100644 --- a/app/assets/stylesheets/new_design/table.scss +++ b/app/assets/stylesheets/new_design/table.scss @@ -20,6 +20,10 @@ padding: (3 * $default-spacer) 2px; } + th.padded { + padding-left: (2 * $default-spacer); + } + &.hoverable { tbody tr:hover { background: $light-grey; diff --git a/app/models/type_de_champ.rb b/app/models/type_de_champ.rb index 77ef1402f..f63d94f34 100644 --- a/app/models/type_de_champ.rb +++ b/app/models/type_de_champ.rb @@ -147,10 +147,7 @@ class TypeDeChamp < ApplicationRecord end def exclude_from_view? - type_champ.in?([ - TypeDeChamp.type_champs.fetch(:explication), - TypeDeChamp.type_champs.fetch(:repetition) - ]) + type_champ == TypeDeChamp.type_champs.fetch(:explication) end def public? diff --git a/app/views/shared/dossiers/_champ_row.html.haml b/app/views/shared/dossiers/_champ_row.html.haml new file mode 100644 index 000000000..361c6e786 --- /dev/null +++ b/app/views/shared/dossiers/_champ_row.html.haml @@ -0,0 +1,40 @@ +- champs.reject(&:exclude_from_view?).each do |c| + - if c.type_champ == TypeDeChamp.type_champs.fetch(:repetition) + %tr + %th.libelle.repetition{ colspan: 3 } + = "#{c.libelle} :" + - c.rows.each do |champs| + = render partial: "shared/dossiers/champ_row", locals: { champs: champs, demande_seen_at: demande_seen_at, profile: profile, repetition: true } + %tr + %th{ colspan: 4 } + - else + %tr + - if c.type_champ == TypeDeChamp.type_champs.fetch(:header_section) + %th.header-section{ colspan: 3 } + = c.libelle + - else + %th.libelle{ class: repetition ? 'padded' : '' } + = "#{c.libelle} :" + %td.rich-text + %span{ class: highlight_if_unseen_class(demande_seen_at, c.updated_at) } + - case c.type_champ + - when TypeDeChamp.type_champs.fetch(:carte) + = render partial: "shared/champs/carte/show", locals: { champ: c } + - when TypeDeChamp.type_champs.fetch(:dossier_link) + = render partial: "shared/champs/dossier_link/show", locals: { champ: c } + - when TypeDeChamp.type_champs.fetch(:multiple_drop_down_list) + = render partial: "shared/champs/multiple_drop_down_list/show", locals: { champ: c } + - when TypeDeChamp.type_champs.fetch(:piece_justificative) + = render partial: "shared/champs/piece_justificative/show", locals: { champ: c } + - when TypeDeChamp.type_champs.fetch(:siret) + = render partial: "shared/champs/siret/show", locals: { champ: c, profile: profile } + - when TypeDeChamp.type_champs.fetch(:textarea) + = render partial: "shared/champs/textarea/show", locals: { champ: c } + - else + = sanitize(c.to_s) + + - if c.type_champ != TypeDeChamp.type_champs.fetch(:header_section) + %td.updated-at + %span{ class: highlight_if_unseen_class(demande_seen_at, c.updated_at) } + modifié le + = c.updated_at.strftime("%d/%m/%Y à %H:%M") diff --git a/app/views/shared/dossiers/_champs.html.haml b/app/views/shared/dossiers/_champs.html.haml index 08c6a311b..1a511730f 100644 --- a/app/views/shared/dossiers/_champs.html.haml +++ b/app/views/shared/dossiers/_champs.html.haml @@ -1,33 +1,3 @@ %table.table.vertical.dossier-champs %tbody - - champs.reject(&:exclude_from_view?).each do |c| - %tr - - if c.type_champ == TypeDeChamp.type_champs.fetch(:header_section) - %th.header-section{ colspan: 3 } - = c.libelle - - else - %th.libelle - = "#{c.libelle} :" - %td.rich-text - %span{ class: highlight_if_unseen_class(demande_seen_at, c.updated_at) } - - case c.type_champ - - when TypeDeChamp.type_champs.fetch(:carte) - = render partial: "shared/champs/carte/show", locals: { champ: c } - - when TypeDeChamp.type_champs.fetch(:dossier_link) - = render partial: "shared/champs/dossier_link/show", locals: { champ: c } - - when TypeDeChamp.type_champs.fetch(:multiple_drop_down_list) - = render partial: "shared/champs/multiple_drop_down_list/show", locals: { champ: c } - - when TypeDeChamp.type_champs.fetch(:piece_justificative) - = render partial: "shared/champs/piece_justificative/show", locals: { champ: c } - - when TypeDeChamp.type_champs.fetch(:siret) - = render partial: "shared/champs/siret/show", locals: { champ: c, profile: profile } - - when TypeDeChamp.type_champs.fetch(:textarea) - = render partial: "shared/champs/textarea/show", locals: { champ: c } - - else - = sanitize(c.to_s) - - - if c.type_champ != TypeDeChamp.type_champs.fetch(:header_section) - %td.updated-at - %span{ class: highlight_if_unseen_class(demande_seen_at, c.updated_at) } - modifié le - = c.updated_at.strftime("%d/%m/%Y à %H:%M") + = render partial: "shared/dossiers/champ_row", locals: { champs: champs, demande_seen_at: demande_seen_at, profile: profile, repetition: false } diff --git a/spec/views/shared/dossiers/_champs.html.haml_spec.rb b/spec/views/shared/dossiers/_champs.html.haml_spec.rb index d8a80ab50..58d6c5690 100644 --- a/spec/views/shared/dossiers/_champs.html.haml_spec.rb +++ b/spec/views/shared/dossiers/_champs.html.haml_spec.rb @@ -8,7 +8,7 @@ describe 'shared/dossiers/champs.html.haml', type: :view do allow(view).to receive(:current_gestionnaire).and_return(gestionnaire) end - subject { render 'shared/dossiers/champs.html.haml', champs: champs, demande_seen_at: demande_seen_at } + subject { render 'shared/dossiers/champs.html.haml', champs: champs, demande_seen_at: demande_seen_at, profile: nil } context "there are some champs" do let(:dossier) { create(:dossier) } From 071448e1d9ef78b83ff34a737811477f487ab1cb Mon Sep 17 00:00:00 2001 From: Paul Chavard Date: Wed, 30 Jan 2019 16:14:15 +0100 Subject: [PATCH 3/3] Champ Repetition dossier editor --- app/assets/stylesheets/new_design/forms.scss | 13 ++++ .../champs/repetition_controller.rb | 21 +++++++ .../new_user/dossiers_controller.rb | 6 +- app/helpers/application_helper.rb | 7 +++ .../new_design/champs/repetition.js | 24 ++++++++ app/javascript/packs/application.js | 1 + app/models/champ.rb | 2 +- app/models/champs/repetition_champ.rb | 12 ++++ app/models/dossier.rb | 16 ++++- app/models/type_de_champ.rb | 4 ++ app/views/champs/repetition/_show.html.haml | 10 ++++ app/views/champs/repetition/show.js.erb | 3 + .../editable_champs/_champ_label.html.haml | 2 +- .../editable_champs/_repetition.html.haml | 16 ++++- config/routes.rb | 1 + spec/features/new_user/brouillon_spec.rb | 35 +++++++++++ spec/models/dossier_spec.rb | 59 +++++++++++++++++++ 17 files changed, 225 insertions(+), 7 deletions(-) create mode 100644 app/controllers/champs/repetition_controller.rb create mode 100644 app/javascript/new_design/champs/repetition.js create mode 100644 app/views/champs/repetition/_show.html.haml create mode 100644 app/views/champs/repetition/show.js.erb diff --git a/app/assets/stylesheets/new_design/forms.scss b/app/assets/stylesheets/new_design/forms.scss index 04617dbfe..e2202ecad 100644 --- a/app/assets/stylesheets/new_design/forms.scss +++ b/app/assets/stylesheets/new_design/forms.scss @@ -111,6 +111,10 @@ } } + .add-row { + margin-bottom: 2 * $default-padding; + } + input[type=checkbox] { &.small-margin { margin-bottom: $default-padding / 2; @@ -246,6 +250,15 @@ .geo-areas { margin-bottom: 2 * $default-padding; } + + &.editable-champ-repetition { + .row { + border-radius: 4px; + border: 1px solid $border-grey; + padding: $default-padding; + margin-bottom: 2 * $default-padding; + } + } } input.aa-input, diff --git a/app/controllers/champs/repetition_controller.rb b/app/controllers/champs/repetition_controller.rb new file mode 100644 index 000000000..c5d8b12ea --- /dev/null +++ b/app/controllers/champs/repetition_controller.rb @@ -0,0 +1,21 @@ +class Champs::RepetitionController < ApplicationController + before_action :authenticate_logged_user! + + def show + @champ = Champ + .joins(:dossier) + .where(dossiers: { user_id: logged_user_ids }) + .find(params[:champ_id]) + + @position = params[:position] + row = (@champ.champs.empty? ? 0 : @champ.champs.last.row) + 1 + + @champ.add_row(row) + + if @champ.private? + @attribute = "dossier[champs_private_attributes][#{@position}][champs_attributes]" + else + @attribute = "dossier[champs_attributes][#{@position}][champs_attributes]" + end + end +end diff --git a/app/controllers/new_user/dossiers_controller.rb b/app/controllers/new_user/dossiers_controller.rb index 7db5821fc..b848ff8ec 100644 --- a/app/controllers/new_user/dossiers_controller.rb +++ b/app/controllers/new_user/dossiers_controller.rb @@ -282,7 +282,8 @@ module NewUser params.permit(dossier: { champs_attributes: [ :id, :value, :primary_value, :secondary_value, :piece_justificative_file, value: [], - etablissement_attributes: Champs::SiretChamp::ETABLISSEMENT_ATTRIBUTES + etablissement_attributes: Champs::SiretChamp::ETABLISSEMENT_ATTRIBUTES, + champs_attributes: [:id, :_destroy, :value, :primary_value, :secondary_value, :piece_justificative_file, value: []] ] }) end @@ -303,8 +304,7 @@ module NewUser end if !save_draft? - errors += @dossier.champs.select(&:mandatory_and_blank?) - .map { |c| "Le champ #{c.libelle.truncate(200)} doit être rempli." } + errors += @dossier.check_mandatory_champs errors += PiecesJustificativesService.missing_pj_error_messages(@dossier) end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index d167a0609..28dc0af2a 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -31,6 +31,13 @@ module ApplicationHelper # rubocop:enable Rails/OutputSafety end + def append_to_element(selector, partial:, locals: {}) + html = escape_javascript(render partial: partial, locals: locals) + # rubocop:disable Rails/OutputSafety + raw("document.querySelector('#{selector}').insertAdjacentHTML('beforeend', \"#{html}\");") + # rubocop:enable Rails/OutputSafety + end + def render_flash(timeout: false, sticky: false, fixed: false) if flash.any? html = render_to_element('#flash_messages', partial: 'layouts/flash_messages', locals: { sticky: sticky, fixed: fixed }, outer: true) diff --git a/app/javascript/new_design/champs/repetition.js b/app/javascript/new_design/champs/repetition.js new file mode 100644 index 000000000..a70b48c71 --- /dev/null +++ b/app/javascript/new_design/champs/repetition.js @@ -0,0 +1,24 @@ +import { delegate } from '@utils'; + +const BUTTON_SELECTOR = '.button.remove-row'; +const DESTROY_INPUT_SELECTOR = 'input[type=hidden][name*=_destroy]'; +const CHAMP_SELECTOR = '.editable-champ'; + +addEventListener('turbolinks:load', () => { + delegate('click', BUTTON_SELECTOR, evt => { + evt.preventDefault(); + + const row = evt.target.closest('.row'); + + for (let input of row.querySelectorAll(DESTROY_INPUT_SELECTOR)) { + input.disabled = false; + input.value = true; + } + for (let champ of row.querySelectorAll(CHAMP_SELECTOR)) { + champ.remove(); + } + + evt.target.remove(); + row.classList.remove('row'); + }); +}); diff --git a/app/javascript/packs/application.js b/app/javascript/packs/application.js index 61ff74ed2..128b7c1b1 100644 --- a/app/javascript/packs/application.js +++ b/app/javascript/packs/application.js @@ -21,6 +21,7 @@ import '../new_design/select2'; import '../new_design/champs/carte'; import '../new_design/champs/linked-drop-down-list'; +import '../new_design/champs/repetition'; import '../new_design/administrateur/champs-editor'; diff --git a/app/models/champ.rb b/app/models/champ.rb index 8a7a9b91c..e0d7b35f1 100644 --- a/app/models/champ.rb +++ b/app/models/champ.rb @@ -10,7 +10,7 @@ class Champ < ApplicationRecord has_many :geo_areas, dependent: :destroy belongs_to :etablissement, dependent: :destroy - delegate :libelle, :type_champ, :order_place, :mandatory?, :description, :drop_down_list, :exclude_from_export?, :exclude_from_view?, to: :type_de_champ + delegate :libelle, :type_champ, :order_place, :mandatory?, :description, :drop_down_list, :exclude_from_export?, :exclude_from_view?, :repetition?, to: :type_de_champ scope :updated_since?, -> (date) { where('champs.updated_at > ?', date) } scope :public_only, -> { where(private: false) } diff --git a/app/models/champs/repetition_champ.rb b/app/models/champs/repetition_champ.rb index 8b06ca396..a2f02fa04 100644 --- a/app/models/champs/repetition_champ.rb +++ b/app/models/champs/repetition_champ.rb @@ -9,10 +9,22 @@ class Champs::RepetitionChamp < Champ champs.group_by(&:row).values end + def add_row(row = 0) + type_de_champ.types_de_champ.each do |type_de_champ| + self.champs << type_de_champ.champ.build(row: row) + end + end + + def mandatory_and_blank? + mandatory? && champs.empty? + end + def search_terms # The user cannot enter any information here so it doesn’t make much sense to search end + private + def setup_dossier champs.each do |champ| champ.dossier = dossier diff --git a/app/models/dossier.rb b/app/models/dossier.rb index 97a98442d..2dee58040 100644 --- a/app/models/dossier.rb +++ b/app/models/dossier.rb @@ -121,7 +121,13 @@ class Dossier < ApplicationRecord def build_default_champs procedure.types_de_champ.each do |type_de_champ| - champs << type_de_champ.champ.build + champ = type_de_champ.champ.build + + if type_de_champ.repetition? + champ.add_row + end + + champs << champ end procedure.types_de_champ_private.each do |type_de_champ| champs_private << type_de_champ.champ.build @@ -334,6 +340,14 @@ class Dossier < ApplicationRecord log_dossier_operation(gestionnaire, :classer_sans_suite) end + def check_mandatory_champs + (champs + champs.select(&:repetition?).flat_map(&:champs)) + .select(&:mandatory_and_blank?) + .map do |champ| + "Le champ #{champ.libelle.truncate(200)} doit être rempli." + end + end + private def log_dossier_operation(gestionnaire, operation, automatic_operation: false) diff --git a/app/models/type_de_champ.rb b/app/models/type_de_champ.rb index f63d94f34..1b167c67a 100644 --- a/app/models/type_de_champ.rb +++ b/app/models/type_de_champ.rb @@ -150,6 +150,10 @@ class TypeDeChamp < ApplicationRecord type_champ == TypeDeChamp.type_champs.fetch(:explication) end + def repetition? + type_champ == TypeDeChamp.type_champs.fetch(:repetition) + end + def public? !private? end diff --git a/app/views/champs/repetition/_show.html.haml b/app/views/champs/repetition/_show.html.haml new file mode 100644 index 000000000..508593712 --- /dev/null +++ b/app/views/champs/repetition/_show.html.haml @@ -0,0 +1,10 @@ +- champs = champ.rows.last +- index = (champ.rows.size - 1) * champs.size +%div{ class: "row row-#{champs.first.row}" } + - champs.each.with_index(index) do |champ, index| + = fields_for "#{attribute}[#{index}]", champ do |form| + = render partial: "shared/dossiers/editable_champs/editable_champ", locals: { champ: champ, form: form } + = form.hidden_field :id + = form.hidden_field :_destroy, disabled: true + %button.button.danger.remove-row + Supprimer diff --git a/app/views/champs/repetition/show.js.erb b/app/views/champs/repetition/show.js.erb new file mode 100644 index 000000000..af552c482 --- /dev/null +++ b/app/views/champs/repetition/show.js.erb @@ -0,0 +1,3 @@ +<%= append_to_element(".repetition-#{@position}", + partial: 'champs/repetition/show', + locals: { champ: @champ, attribute: @attribute }) %> diff --git a/app/views/shared/dossiers/editable_champs/_champ_label.html.haml b/app/views/shared/dossiers/editable_champs/_champ_label.html.haml index 6ba997ae9..43c6d2b11 100644 --- a/app/views/shared/dossiers/editable_champs/_champ_label.html.haml +++ b/app/views/shared/dossiers/editable_champs/_champ_label.html.haml @@ -1,4 +1,4 @@ -= form.label champ.main_value_name do += form.label champ.main_value_name, { class: champ.repetition? ? 'header-section' : '' } do #{champ.libelle} - if champ.mandatory? %span.mandatory * diff --git a/app/views/shared/dossiers/editable_champs/_repetition.html.haml b/app/views/shared/dossiers/editable_champs/_repetition.html.haml index b9fcfa711..d7e8717c7 100644 --- a/app/views/shared/dossiers/editable_champs/_repetition.html.haml +++ b/app/views/shared/dossiers/editable_champs/_repetition.html.haml @@ -1 +1,15 @@ -%h2.repetition-libelle= champ.libelle +%div{ class: "repetition-#{form.index}" } + - champ.rows.each do |champs| + %div{ class: "row row-#{champs.first.row}" } + - champs.each do |champ| + = 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? + = 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} »" diff --git a/config/routes.rb b/config/routes.rb index 570844167..c0b41153f 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -127,6 +127,7 @@ Rails.application.routes.draw do get ':position/siret', to: 'siret#show', as: :siret get ':position/dossier_link', to: 'dossier_link#show', as: :dossier_link post ':position/carte', to: 'carte#show', as: :carte + post ':position/repetition', to: 'repetition#show', as: :repetition end get 'tour-de-france' => 'root#tour_de_france' diff --git a/spec/features/new_user/brouillon_spec.rb b/spec/features/new_user/brouillon_spec.rb index 660d084a8..16c2d82ba 100644 --- a/spec/features/new_user/brouillon_spec.rb +++ b/spec/features/new_user/brouillon_spec.rb @@ -83,6 +83,41 @@ feature 'The user' do expect(page).to have_field('dossier_link', with: '123') end + let(:procedure_with_repetition) do + tdc = create(:type_de_champ_repetition, libelle: 'repetition') + tdc.types_de_champ << create(:type_de_champ_text, libelle: 'text') + create(:procedure, :published, :for_individual, types_de_champ: [tdc]) + end + + scenario 'fill a dossier with repetition', js: true do + log_in(user.email, password, procedure_with_repetition) + + fill_individual + + fill_in('text', with: 'super texte') + expect(page).to have_field('text', with: 'super texte') + + click_on 'Ajouter une ligne pour' + + within '.row-1' do + fill_in('text', with: 'un autre texte') + end + + expect(page).to have_content('Supprimer', count: 2) + + click_on 'Enregistrer le brouillon' + + expect(page).to have_content('Supprimer', count: 2) + + within '.row-1' do + click_on 'Supprimer' + end + + click_on 'Enregistrer le brouillon' + + expect(page).to have_content('Supprimer', count: 1) + end + let(:simple_procedure) do tdcs = [create(:type_de_champ, mandatory: true, libelle: 'texte obligatoire')] create(:procedure, :published, :for_individual, types_de_champ: tdcs) diff --git a/spec/models/dossier_spec.rb b/spec/models/dossier_spec.rb index c5a7ee826..451fdffbe 100644 --- a/spec/models/dossier_spec.rb +++ b/spec/models/dossier_spec.rb @@ -832,4 +832,63 @@ describe Dossier do it { expect(dossier.followers_gestionnaires).not_to include(gestionnaire) } it { expect(dossier.dossier_operation_logs.pluck(:gestionnaire_id, :operation, :automatic_operation)).to match([[nil, 'passer_en_instruction', true]]) } end + + describe "#check_mandatory_champs" do + let(:procedure) { create(:procedure, :with_type_de_champ) } + let(:dossier) { create(:dossier, :with_all_champs, procedure: procedure) } + + it 'no mandatory champs' do + expect(dossier.check_mandatory_champs).to be_empty + end + + context "with mandatory champs" do + let(:procedure) { create(:procedure, :with_type_de_champ_mandatory) } + let(:champ_with_error) { dossier.champs.first } + + before do + champ_with_error.value = nil + champ_with_error.save + end + + it 'should have errors' do + errors = dossier.check_mandatory_champs + expect(errors).not_to be_empty + expect(errors.first).to eq("Le champ #{champ_with_error.libelle} doit être rempli.") + end + end + + context "with champ repetition" do + let(:procedure) { create(:procedure) } + let(:type_de_champ_repetition) { create(:type_de_champ_repetition, mandatory: true) } + + before do + procedure.types_de_champ << type_de_champ_repetition + type_de_champ_repetition.types_de_champ << create(:type_de_champ_text, mandatory: true) + end + + context "when no champs" do + let(:champ_with_error) { dossier.champs.first } + + it 'should have errors' do + errors = dossier.check_mandatory_champs + expect(errors).not_to be_empty + expect(errors.first).to eq("Le champ #{champ_with_error.libelle} doit être rempli.") + end + end + + context "when mandatory champ inside repetition" do + let(:champ_with_error) { dossier.champs.first.champs.first } + + before do + dossier.champs.first.add_row + end + + it 'should have errors' do + errors = dossier.check_mandatory_champs + expect(errors).not_to be_empty + expect(errors.first).to eq("Le champ #{champ_with_error.libelle} doit être rempli.") + end + end + end + end end