diff --git a/app/helpers/procedure_helper.rb b/app/helpers/procedure_helper.rb index 443626b8e..fe3395201 100644 --- a/app/helpers/procedure_helper.rb +++ b/app/helpers/procedure_helper.rb @@ -89,6 +89,9 @@ module ProcedureHelper .merge(include: TYPES_DE_CHAMP_INCLUDE.merge(types_de_champ: TYPES_DE_CHAMP_BASE)) def types_de_champ_as_json(types_de_champ) - types_de_champ.as_json(TYPES_DE_CHAMP) + types_de_champ.includes(:drop_down_list, + piece_justificative_template_attachment: :blob, + types_de_champ: [:drop_down_list, piece_justificative_template_attachment: :blob]) + .as_json(TYPES_DE_CHAMP) end end diff --git a/app/javascript/new_design/administrateur/DraggableItem.js b/app/javascript/new_design/administrateur/DraggableItem.js index e1ff2a357..95d284120 100644 --- a/app/javascript/new_design/administrateur/DraggableItem.js +++ b/app/javascript/new_design/administrateur/DraggableItem.js @@ -130,9 +130,7 @@ export default { addChamp() { this.typesDeChamp.push({ type_champ: 'text', - drop_down_list: {}, - types_de_champ: [], - options: {} + types_de_champ: [] }); } } diff --git a/app/javascript/new_design/administrateur/champs-editor.js b/app/javascript/new_design/administrateur/champs-editor.js index 8445b458d..3b47de908 100644 --- a/app/javascript/new_design/administrateur/champs-editor.js +++ b/app/javascript/new_design/administrateur/champs-editor.js @@ -53,6 +53,14 @@ function initEditor(el) { this.update = update; this.updateAll = updateAll; + + // We add an initial type de champ here if form is empty + if (this.state.typesDeChamp.length === 0) { + this.state.typesDeChamp.push({ + type_champ: 'text', + types_de_champ: [] + }); + } } }); } diff --git a/app/javascript/new_design/champs/linked-drop-down-list.js b/app/javascript/new_design/champs/linked-drop-down-list.js index c49d3dcd7..c1e14d50d 100644 --- a/app/javascript/new_design/champs/linked-drop-down-list.js +++ b/app/javascript/new_design/champs/linked-drop-down-list.js @@ -1,29 +1,28 @@ -addEventListener('turbolinks:load', () => { - const primaries = document.querySelectorAll('select[data-secondary-options]'); +import { delegate } from '@utils'; - for (let primary of primaries) { - let secondary = document.querySelector( - `select[data-secondary-id="${primary.dataset.primaryId}"]` - ); - let secondaryOptions = JSON.parse(primary.dataset.secondaryOptions); +const PRIMARY_SELECTOR = 'select[data-secondary-options]'; +const SECONDARY_SELECTOR = 'select[data-secondary]'; +const CHAMP_SELECTOR = '.editable-champ'; - primary.addEventListener('change', e => { - let option, options, element; +delegate('change', PRIMARY_SELECTOR, evt => { + const primary = evt.target; + const secondary = primary + .closest(CHAMP_SELECTOR) + .querySelector(SECONDARY_SELECTOR); + const options = JSON.parse(primary.dataset.secondaryOptions); - while ((option = secondary.firstChild)) { - secondary.removeChild(option); - } - - options = secondaryOptions[e.target.value]; - - for (let option of options) { - element = document.createElement('option'); - element.textContent = option; - element.value = option; - secondary.appendChild(element); - } - - secondary.selectedIndex = 0; - }); - } + selectOptions(secondary, options[primary.value]); }); + +function selectOptions(selectElement, options) { + selectElement.innerHTML = ''; + + for (let option of options) { + let element = document.createElement('option'); + element.textContent = option; + element.value = option; + selectElement.appendChild(element); + } + + selectElement.selectedIndex = 0; +} diff --git a/app/javascript/new_design/champs/repetition.js b/app/javascript/new_design/champs/repetition.js index a70b48c71..b6a380f1d 100644 --- a/app/javascript/new_design/champs/repetition.js +++ b/app/javascript/new_design/champs/repetition.js @@ -4,21 +4,19 @@ 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(); +delegate('click', BUTTON_SELECTOR, evt => { + evt.preventDefault(); - const row = evt.target.closest('.row'); + 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(); - } + 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'); - }); + evt.target.remove(); + row.classList.remove('row'); }); diff --git a/app/models/champs/linked_drop_down_list_champ.rb b/app/models/champs/linked_drop_down_list_champ.rb index cb4ff4ae8..d28b3004f 100644 --- a/app/models/champs/linked_drop_down_list_champ.rb +++ b/app/models/champs/linked_drop_down_list_champ.rb @@ -30,7 +30,7 @@ class Champs::LinkedDropDownListChamp < Champ end def to_s - value.present? ? [primary_value, secondary_value].compact.join(' / ') : "" + value.present? ? [primary_value, secondary_value].select(&:present?).join(' / ') : "" end def for_export diff --git a/app/models/concerns/tags_substitution_concern.rb b/app/models/concerns/tags_substitution_concern.rb index 5b15236d1..1162beb0e 100644 --- a/app/models/concerns/tags_substitution_concern.rb +++ b/app/models/concerns/tags_substitution_concern.rb @@ -167,13 +167,11 @@ module TagsSubstitutionConcern end def types_de_champ_tags(types_de_champ, available_for_states) - types_de_champ.map do |tdc| - { - libelle: tdc.libelle, - description: tdc.description, - available_for_states: available_for_states - } + tags = types_de_champ.flat_map(&:tags_for_template) + tags.each do |tag| + tag[:available_for_states] = available_for_states end + tags end def replace_tags(text, dossier) @@ -181,10 +179,9 @@ module TagsSubstitutionConcern return '' end - text = replace_type_de_champ_tags(text, filter_tags(champ_public_tags), dossier.champs) - text = replace_type_de_champ_tags(text, filter_tags(champ_private_tags), dossier.champs_private) - tags_and_datas = [ + [champ_public_tags, dossier.champs], + [champ_private_tags, dossier.champs_private], [dossier_tags, dossier], [INDIVIDUAL_TAGS, dossier.individual], [ENTREPRISE_TAGS, dossier.etablissement&.entreprise] @@ -195,38 +192,29 @@ module TagsSubstitutionConcern .inject(text) { |acc, (tags, data)| replace_tags_with_values_from_data(acc, tags, data) } end - def replace_type_de_champ_tags(text, types_de_champ, dossier_champs) - types_de_champ.inject(text) do |acc, tag| - champ = dossier_champs - .select { |dossier_champ| dossier_champ.libelle == tag[:libelle] } - .first - - replace_tag(acc, tag, champ) - end - end - def replace_tags_with_values_from_data(text, tags, data) if data.present? tags.inject(text) do |acc, tag| - if tag.key?(:target) - value = data.send(tag[:target]) - else - value = instance_exec(data, &tag[:lambda]) - end - replace_tag(acc, tag, value) + replace_tag(acc, tag, data) end else text end end - def replace_tag(text, tag, value) + def replace_tag(text, tag, data) libelle = Regexp.quote(tag[:libelle]) # allow any kind of space (non-breaking or other) in the tag’s libellé to match any kind of space in the template # (the '\\ |' is there because plain ASCII spaces were escaped by preceding Regexp.quote) libelle.gsub!(/\\ |[[:blank:]]/, "[[:blank:]]") + if tag.key?(:target) + value = data.send(tag[:target]) + else + value = instance_exec(data, &tag[:lambda]) + end + text.gsub(/--#{libelle}--/, value.to_s) end end diff --git a/app/models/dossier.rb b/app/models/dossier.rb index 2dee58040..f35370ed4 100644 --- a/app/models/dossier.rb +++ b/app/models/dossier.rb @@ -186,7 +186,7 @@ class Dossier < ApplicationRecord "Dossier en brouillon répondant à la démarche ", procedure.libelle, " gérée par l'organisme ", - procedure.organisation + procedure.organisation_name ] else parts = [ @@ -195,7 +195,7 @@ class Dossier < ApplicationRecord " sur la démarche ", procedure.libelle, " gérée par l'organisme ", - procedure.organisation + procedure.organisation_name ] end diff --git a/app/models/type_de_champ.rb b/app/models/type_de_champ.rb index 1b167c67a..727767c6e 100644 --- a/app/models/type_de_champ.rb +++ b/app/models/type_de_champ.rb @@ -38,6 +38,7 @@ class TypeDeChamp < ApplicationRecord has_many :types_de_champ, -> { ordered }, foreign_key: :parent_id, class_name: 'TypeDeChamp', dependent: :destroy store_accessor :options, :cadastres, :quartiers_prioritaires, :parcelles_agricoles, :old_pj + delegate :tags_for_template, to: :dynamic_type # TODO simplify after migrating `options` column to (non YAML encoded) JSON class MaybeYaml diff --git a/app/models/types_de_champ/linked_drop_down_list_type_de_champ.rb b/app/models/types_de_champ/linked_drop_down_list_type_de_champ.rb index a90b7a0f0..50f4aa111 100644 --- a/app/models/types_de_champ/linked_drop_down_list_type_de_champ.rb +++ b/app/models/types_de_champ/linked_drop_down_list_type_de_champ.rb @@ -21,6 +21,34 @@ class TypesDeChamp::LinkedDropDownListTypeDeChamp < TypesDeChamp::TypeDeChampBas secondary_options end + def tags_for_template + tags = super + l = libelle + tags.push( + { + libelle: "#{l}/primaire", + description: "#{description} (menu primaire)", + lambda: -> (champs) { + champs + .detect { |champ| champ.libelle == l } + &.primary_value + } + } + ) + tags.push( + { + libelle: "#{l}/secondaire", + description: "#{description} (menu secondaire)", + lambda: -> (champs) { + champs + .detect { |champ| champ.libelle == l } + &.secondary_value + } + } + ) + tags + end + private def check_presence_of_primary_options 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 4f77596c3..fb05bd2ab 100644 --- a/app/models/types_de_champ/type_de_champ_base.rb +++ b/app/models/types_de_champ/type_de_champ_base.rb @@ -1,9 +1,22 @@ class TypesDeChamp::TypeDeChampBase include ActiveModel::Validations - delegate :libelle, to: :@type_de_champ + delegate :description, :libelle, to: :@type_de_champ def initialize(type_de_champ) @type_de_champ = type_de_champ end + + def tags_for_template + l = libelle + [ + { + libelle: l, + description: description, + lambda: -> (champs) { + champs.detect { |champ| champ.libelle == l } + } + } + ] + end end diff --git a/app/services/clamav_service.rb b/app/services/clamav_service.rb index 1b6ea0c19..7acae2a0a 100644 --- a/app/services/clamav_service.rb +++ b/app/services/clamav_service.rb @@ -1,15 +1,21 @@ class ClamavService def self.safe_file?(file_path) if Rails.env.development? - Rails.logger.info("Rails.env = development => fake scan") # FIXME : remove me return true end FileUtils.chmod(0666, file_path) client = ClamAV::Client.new - response = client.execute(ClamAV::Commands::ScanCommand.new(file_path)) - Rails.logger.info("ClamAV response for #{file_path} : #{response.first.class.name}") # FIXME : remove me - response.first.class != ClamAV::VirusResponse + response = client.execute(ClamAV::Commands::ScanCommand.new(file_path)).first + if response.class == ClamAV::SuccessResponse + true + elsif response.class == ClamAV::VirusResponse + false + elsif response.class == ClamAV::ErrorResponse + raise "ClamAV ErrorResponse : #{response.error_str}" + else + raise "ClamAV unkown response #{response.class.name}" + end end end diff --git a/app/views/shared/dossiers/editable_champs/_linked_drop_down_list.html.haml b/app/views/shared/dossiers/editable_champs/_linked_drop_down_list.html.haml index 03afe6832..fcabeefa9 100644 --- a/app/views/shared/dossiers/editable_champs/_linked_drop_down_list.html.haml +++ b/app/views/shared/dossiers/editable_champs/_linked_drop_down_list.html.haml @@ -1,10 +1,9 @@ - if champ.drop_down_list && champ.drop_down_list.options.any? - - champ_id = champ.object_id = form.select :primary_value, champ.primary_options, { required: champ.mandatory? }, - { data: { "secondary-options" => champ.secondary_options, "primary-id" => champ_id } } + { data: { secondary_options: champ.secondary_options } } = form.select :secondary_value, champ.secondary_options[champ.primary_value], { required: champ.mandatory? }, - { data: { "secondary-id" => champ_id } } + { data: { secondary: true } } diff --git a/spec/features/admin/procedure_creation_spec.rb b/spec/features/admin/procedure_creation_spec.rb index f8fd9a74a..eefc0853f 100644 --- a/spec/features/admin/procedure_creation_spec.rb +++ b/spec/features/admin/procedure_creation_spec.rb @@ -100,9 +100,6 @@ feature 'As an administrateur I wanna create a new procedure', js: true do page.refresh expect(page).to have_current_path(champs_procedure_path(Procedure.last)) - within '.footer' do - click_on 'Ajouter un champ' - end expect(page).to have_selector('#procedure_types_de_champ_attributes_0_libelle') fill_in 'procedure_types_de_champ_attributes_0_libelle', with: 'libelle de champ' expect(page).to have_content('Formulaire mis à jour') @@ -131,9 +128,6 @@ feature 'As an administrateur I wanna create a new procedure', js: true do scenario 'After adding champ and file, make publication' do page.refresh - within '.footer' do - click_on 'Ajouter un champ' - end fill_in 'procedure_types_de_champ_attributes_0_libelle', with: 'libelle de champ' expect(page).to have_content('Formulaire mis à jour') diff --git a/spec/features/new_administrateur/procedures_spec.rb b/spec/features/new_administrateur/procedures_spec.rb index 47ba7e66f..c1b3b1b8b 100644 --- a/spec/features/new_administrateur/procedures_spec.rb +++ b/spec/features/new_administrateur/procedures_spec.rb @@ -11,6 +11,8 @@ feature 'As an administrateur I edit procedure', js: true do end it "Add a new champ" do + click_on 'Supprimer' + within '.footer' do click_on 'Ajouter un champ' end @@ -31,7 +33,6 @@ feature 'As an administrateur I edit procedure', js: true do click_on 'Ajouter un champ' click_on 'Ajouter un champ' click_on 'Ajouter un champ' - click_on 'Ajouter un champ' end expect(page).not_to have_content('Le libellé doit être rempli.') expect(page).not_to have_content('Modifications non sauvegardées.') @@ -66,9 +67,6 @@ feature 'As an administrateur I edit procedure', js: true do end it "Remove champs" do - within '.footer' do - click_on 'Ajouter un champ' - end fill_in 'procedure_types_de_champ_attributes_0_libelle', with: 'libellé de champ' expect(page).to have_content('Formulaire mis à jour') page.refresh @@ -78,13 +76,10 @@ feature 'As an administrateur I edit procedure', js: true do expect(page).not_to have_content('Supprimer') page.refresh - expect(page).not_to have_content('Supprimer') + expect(page).to have_content('Supprimer', count: 1) end it "Only add valid champs" do - within '.footer' do - click_on 'Ajouter un champ' - end expect(page).to have_selector('#procedure_types_de_champ_attributes_0_description') fill_in 'procedure_types_de_champ_attributes_0_description', with: 'déscription du champ' expect(page).to have_content('Le libellé doit être rempli.') @@ -95,9 +90,6 @@ feature 'As an administrateur I edit procedure', js: true do end it "Add repetition champ" do - within '.footer' do - click_on 'Ajouter un champ' - end expect(page).to have_selector('#procedure_types_de_champ_attributes_0_libelle') select('Bloc répétable', from: 'procedure_types_de_champ_attributes_0_type_champ') fill_in 'procedure_types_de_champ_attributes_0_libelle', with: 'libellé de champ' diff --git a/spec/features/new_user/linked_dropdown_spec.rb b/spec/features/new_user/linked_dropdown_spec.rb index 769057fcd..e9940f43d 100644 --- a/spec/features/new_user/linked_dropdown_spec.rb +++ b/spec/features/new_user/linked_dropdown_spec.rb @@ -74,7 +74,8 @@ feature 'linked dropdown lists' do def secondary_id_for(libelle) primary_id = primary_id_for(libelle) - link = find("\##{primary_id}")['data-primary-id'] - find("[data-secondary-id=\"#{link}\"]")['id'] + find("\##{primary_id}") + .ancestor('.editable-champ') + .find("[data-secondary]")['id'] end end diff --git a/spec/models/concern/tags_substitution_concern_spec.rb b/spec/models/concern/tags_substitution_concern_spec.rb index f7b95edf2..b2a640214 100644 --- a/spec/models/concern/tags_substitution_concern_spec.rb +++ b/spec/models/concern/tags_substitution_concern_spec.rb @@ -110,6 +110,40 @@ describe TagsSubstitutionConcern, type: :model do end end + context 'when the procedure has a linked drop down menus type de champ' do + let(:types_de_champ) do + [ + create(:type_de_champ_linked_drop_down_list, libelle: 'libelle') + ] + end + + let(:template) { 'tout : --libelle--, primaire : --libelle/primaire--, secondaire : --libelle/secondaire--' } + + context 'and the champ has no value' do + it { is_expected.to eq('tout : , primaire : , secondaire : ') } + end + + context 'and the champ has a primary value' do + before do + c = dossier.champs.detect { |champ| champ.libelle == 'libelle' } + c.primary_value = 'primo' + c.save + end + it { is_expected.to eq('tout : primo, primaire : primo, secondaire : ') } + end + + context 'and the champ has a primary and secondary value' do + before do + c = dossier.champs.detect { |champ| champ.libelle == 'libelle' } + c.primary_value = 'primo' + c.secondary_value = 'secundo' + c.save + end + + it { is_expected.to eq('tout : primo / secundo, primaire : primo, secondaire : secundo') } + end + end + context 'when the dossier has a motivation' do let(:dossier) { create(:dossier, motivation: 'motivation') } diff --git a/spec/models/dossier_spec.rb b/spec/models/dossier_spec.rb index 451fdffbe..60e8dab8c 100644 --- a/spec/models/dossier_spec.rb +++ b/spec/models/dossier_spec.rb @@ -236,14 +236,15 @@ describe Dossier do end describe "#text_summary" do - let(:procedure) { create(:procedure, libelle: "Démarche", organisation: "Organisme") } + let(:service) { create(:service, nom: 'nom du service') } + let(:procedure) { create(:procedure, libelle: "Démarche", organisation: "Organisme", service: service) } context 'when the dossier has been en_construction' do let(:dossier) { create :dossier, procedure: procedure, state: Dossier.states.fetch(:en_construction), en_construction_at: "31/12/2010".to_date } subject { dossier.text_summary } - it { is_expected.to eq("Dossier déposé le 31/12/2010 sur la démarche Démarche gérée par l'organisme Organisme") } + it { is_expected.to eq("Dossier déposé le 31/12/2010 sur la démarche Démarche gérée par l'organisme nom du service") } end context 'when the dossier has not been en_construction' do @@ -251,7 +252,7 @@ describe Dossier do subject { dossier.text_summary } - it { is_expected.to eq("Dossier en brouillon répondant à la démarche Démarche gérée par l'organisme Organisme") } + it { is_expected.to eq("Dossier en brouillon répondant à la démarche Démarche gérée par l'organisme nom du service") } end end diff --git a/spec/services/clamav_service_spec.rb b/spec/services/clamav_service_spec.rb index be77ccc3f..4f843e620 100644 --- a/spec/services/clamav_service_spec.rb +++ b/spec/services/clamav_service_spec.rb @@ -4,17 +4,27 @@ describe ClamavService do describe '.safe_file?' do let(:path_file) { '/tmp/plop.txt' } - subject { ClamavService.safe_file? path_file } + subject { ClamavService.safe_file?(path_file) } before do - client = instance_double("ClamAV::Client", :execute => [ClamAV::SuccessResponse]) + client = double("ClamAV::Client", execute: [response]) allow(ClamAV::Client).to receive(:new).and_return(client) + allow(FileUtils).to receive(:chmod).with(0666, path_file).and_return(true) end - it 'change permission of file path' do - allow(FileUtils).to receive(:chmod).with(0666, path_file).and_return(true) + context 'When response type is ClamAV::SuccessResponse' do + let(:response) { ClamAV::SuccessResponse.new("OK") } + it { expect(subject).to eq(true) } + end - subject + context 'When response type is ClamAV::VirusResponse' do + let(:response) { ClamAV::VirusResponse.new("KO", "VirusN4ame") } + it { expect(subject).to eq(false) } + end + + context 'When response type is ClamAV::ErrorResponse' do + let(:response) { ClamAV::ErrorResponse.new("File not found") } + it { expect { subject }.to raise_error("ClamAV ErrorResponse : File not found") } end end end