diff --git a/app/assets/javascripts/new_design/champs/linked_drop_down_list.js b/app/assets/javascripts/new_design/champs/linked_drop_down_list.js new file mode 100644 index 000000000..d82bf438f --- /dev/null +++ b/app/assets/javascripts/new_design/champs/linked_drop_down_list.js @@ -0,0 +1,30 @@ +document.addEventListener('turbolinks:load', function() { + var primaries, i, primary, secondary, secondaryOptions; + + primaries = document.querySelectorAll('select[data-secondary-options]'); + for (i = 0; i < primaries.length; i++) { + primary = primaries[i]; + secondary = document.querySelector('select[data-secondary-id="' + primary.dataset.primaryId + '"]'); + secondaryOptions = JSON.parse(primary.dataset.secondaryOptions); + + primary.addEventListener('change', function(e) { + var option, options, element; + + while ((option = secondary.firstChild)) { + secondary.removeChild(option); + } + + options = secondaryOptions[e.target.value]; + + for (i = 0; i < options.length; i++) { + option = options[i]; + element = document.createElement("option"); + element.textContent = option; + element.value = option; + secondary.appendChild(element); + } + + secondary.selectedIndex = 0; + }); + } +}); diff --git a/app/controllers/new_user/dossiers_controller.rb b/app/controllers/new_user/dossiers_controller.rb index 905f78ebe..361eb3765 100644 --- a/app/controllers/new_user/dossiers_controller.rb +++ b/app/controllers/new_user/dossiers_controller.rb @@ -145,7 +145,7 @@ module NewUser def champs_params params.permit(dossier: { champs_attributes: [ - :id, :value, :piece_justificative_file, value: [], + :id, :value, :primary_value, :secondary_value, :piece_justificative_file, value: [], etablissement_attributes: Champs::SiretChamp::ETABLISSEMENT_ATTRIBUTES ] }) diff --git a/app/models/champ.rb b/app/models/champ.rb index f396e988c..dc8007442 100644 --- a/app/models/champ.rb +++ b/app/models/champ.rb @@ -10,6 +10,7 @@ class Champ < ApplicationRecord scope :updated_since?, -> (date) { where('champs.updated_at > ?', date) } scope :public_only, -> { where(private: false) } scope :private_only, -> { where(private: true) } + scope :ordered, -> { includes(:type_de_champ).order('types_de_champ.order_place') } def public? !private? @@ -35,6 +36,10 @@ class Champ < ApplicationRecord end end + def main_value_name + :value + end + private def string_value diff --git a/app/models/champs/linked_drop_down_list_champ.rb b/app/models/champs/linked_drop_down_list_champ.rb index 20fb7455a..ef02e78f4 100644 --- a/app/models/champs/linked_drop_down_list_champ.rb +++ b/app/models/champs/linked_drop_down_list_champ.rb @@ -1,2 +1,36 @@ class Champs::LinkedDropDownListChamp < Champ + attr_reader :primary_value, :secondary_value + delegate :primary_options, :secondary_options, to: :type_de_champ + + after_initialize :unpack_value + + def unpack_value + if value.present? + primary, secondary = JSON.parse(value) + else + primary = secondary = '' + end + @primary_value ||= primary + @secondary_value ||= secondary + end + + def primary_value=(value) + @primary_value = value + pack_value + end + + def secondary_value=(value) + @secondary_value = value + pack_value + end + + def main_value_name + :primary_value + end + + private + + def pack_value + self.value = JSON.generate([ primary_value, secondary_value ]) + end end diff --git a/app/models/dossier.rb b/app/models/dossier.rb index 646aee849..c02facd83 100644 --- a/app/models/dossier.rb +++ b/app/models/dossier.rb @@ -104,15 +104,11 @@ class Dossier < ApplicationRecord end def ordered_champs - # TODO: use the line below when the procedure preview does not leak champ with dossier_id == 0 - # champs.joins(:type_de_champ).order('types_de_champ.order_place') - champs.joins(', types_de_champ').where("champs.type_de_champ_id = types_de_champ.id AND types_de_champ.procedure_id = #{procedure.id}").order('order_place') + champs.ordered end def ordered_champs_private - # TODO: use the line below when the procedure preview does not leak champ with dossier_id == 0 - # champs_private.includes(:type_de_champ).order('types_de_champ.order_place') - champs_private.joins(', types_de_champ').where("champs.type_de_champ_id = types_de_champ.id AND types_de_champ.procedure_id = #{procedure.id}").order('order_place') + champs_private.ordered end def ordered_pieces_justificatives 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 e54cbef7f..6e602a97f 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 @@ -1,2 +1,31 @@ class TypesDeChamp::LinkedDropDownListTypeDeChamp < TypeDeChamp + PRIMARY_PATTERN = /^--(.*)--$/ + + def primary_options + primary_options = unpack_options.map(&:first) + if primary_options.present? + primary_options.unshift('') + end + primary_options + end + + def secondary_options + secondary_options = unpack_options.to_h + if secondary_options.present? + secondary_options[''] = [] + end + secondary_options + end + + private + + def unpack_options + _, *options = drop_down_list.options + chunked = options.slice_before(PRIMARY_PATTERN) + chunked.map do |chunk| + primary, *secondary = chunk + secondary.unshift('') + [PRIMARY_PATTERN.match(primary)[1], secondary] + end + end end diff --git a/app/views/layouts/_new_header.haml b/app/views/layouts/_new_header.haml index dad1e8d20..a5c6f5883 100644 --- a/app/views/layouts/_new_header.haml +++ b/app/views/layouts/_new_header.haml @@ -36,7 +36,7 @@ - if nav_bar_profile == :user %ul.header-tabs %li - = active_link_to "Dossiers", dossiers_path, active: :inclusive, class: 'tab-link' + = active_link_to "Dossiers", users_dossiers_path, active: :inclusive, class: 'tab-link' %ul.header-right-content - if nav_bar_profile == :gestionnaire && gestionnaire_signed_in? diff --git a/app/views/layouts/new_application.html.haml b/app/views/layouts/new_application.html.haml index affb3ad19..a6a10c443 100644 --- a/app/views/layouts/new_application.html.haml +++ b/app/views/layouts/new_application.html.haml @@ -17,6 +17,9 @@ = stylesheet_link_tag "new_design/new_application", media: "all", "data-turbolinks-track": "reload" = stylesheet_link_tag "new_design/print", media: "print", "data-turbolinks-track": true + - if Rails.env.development? + = stylesheet_link_tag :xray + %body .page-wrapper = render partial: "layouts/support_navigator_banner" @@ -35,6 +38,10 @@ = render partial: "layouts/mailjet_newsletter" = javascript_include_tag "new_design/application", "data-turbolinks-eval": false + + - if Rails.env.development? + = javascript_include_tag :xray + = yield :charts_js - if Rails.env == "test" %script{ type: "text/javascript" } 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 8c1070299..6ba997ae9 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 :value do += form.label champ.main_value_name do #{champ.libelle} - if champ.mandatory? %span.mandatory * 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 new file mode 100644 index 000000000..03afe6832 --- /dev/null +++ b/app/views/shared/dossiers/editable_champs/_linked_drop_down_list.html.haml @@ -0,0 +1,10 @@ +- 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 } } + = form.select :secondary_value, + champ.secondary_options[champ.primary_value], + { required: champ.mandatory? }, + { data: { "secondary-id" => champ_id } } diff --git a/app/views/users/sessions/new.html.haml b/app/views/users/sessions/new.html.haml index 3412a0439..943afd7a3 100644 --- a/app/views/users/sessions/new.html.haml +++ b/app/views/users/sessions/new.html.haml @@ -50,7 +50,7 @@ = link_to "", france_connect_particulier_path, class: "login-with-fc" .center - = link_to "Qu’est-ce que FranceConnect ?", "https://franceconnect.gouv.fr/", target: "_blank", class: "link" + = link_to "Qu’est-ce que FranceConnect ?", "https://app.franceconnect.gouv.fr/en-savoir-plus", target: "_blank", class: "link" - if resource_name == :user %hr diff --git a/lib/tasks/2018_05_21_cerfa_to_pj.rake b/lib/tasks/2018_05_21_cerfa_to_pj.rake index 2dcabd4c9..59406195a 100644 --- a/lib/tasks/2018_05_21_cerfa_to_pj.rake +++ b/lib/tasks/2018_05_21_cerfa_to_pj.rake @@ -3,9 +3,9 @@ namespace :'2018_05_21_cerfa_to_pj' do dossiers = Cerfa.includes(dossier: [:procedure]).all.reject(&:empty?).map(&:dossier).compact.uniq dossiers.group_by(&:procedure).each do |procedure, dossiers| - if !procedure.type_de_champs.find_by(libelle: 'CERFA') + if !procedure.types_de_champ.find_by(libelle: 'CERFA') procedure.administrateur.enable_feature(:champ_pj) - type_de_champ = procedure.type_de_champs.create( + type_de_champ = procedure.types_de_champ.create( type_champ: 'piece_justificative', libelle: 'CERFA' ) diff --git a/lib/tasks/support.rake b/lib/tasks/support.rake index 2f70a7816..f4bbb82cc 100644 --- a/lib/tasks/support.rake +++ b/lib/tasks/support.rake @@ -31,4 +31,21 @@ namespace :support do pp.update(administrateur: new_owner) end end + + desc <<~EOD + Delete the user account for a given USER_MAIL. + Only works if the user has no dossier where the instruction has started. + EOD + task delete_user_account: :environment do + user_mail = ENV['USER_MAIL'] + if user_mail.nil? + fail "Must specify a USER_MAIL" + end + user = User.find_by(email: user_mail) + if user.dossiers.state_instruction_commencee.any? + fail "Cannot delete this user because instruction has started for some dossiers" + end + user.dossiers.each { |d| d.delete_and_keep_track } + user.destroy + end end diff --git a/spec/features/new_user/linked_dropdown_spec.rb b/spec/features/new_user/linked_dropdown_spec.rb new file mode 100644 index 000000000..01758f0bc --- /dev/null +++ b/spec/features/new_user/linked_dropdown_spec.rb @@ -0,0 +1,76 @@ +require 'spec_helper' + +feature 'linked dropdown lists' do + let(:password) { 'secret_password' } + let!(:user) { create(:user, password: password) } + + let(:list_items) do + <<~END_OF_LIST + --Primary 1-- + Secondary 1.1 + Secondary 1.2 + --Primary 2-- + Secondary 2.1 + Secondary 2.2 + Secondary 2.3 + END_OF_LIST + end + let(:drop_down_list) { create(:drop_down_list, value: list_items) } + let(:type_de_champ) { create(:type_de_champ_linked_drop_down_list, libelle: 'linked dropdown', drop_down_list: drop_down_list) } + + let!(:procedure) do + p = create(:procedure, :published, :for_individual) + p.types_de_champ << type_de_champ + p + end + + let(:user_dossier) { user.dossiers.first } + + scenario 'change primary value, secondary options are updated', js: true do + log_in(user.email, password, procedure) + + fill_individual + + # Select a primary value + select('Primary 2', from: primary_id_for('linked dropdown')) + + # Secondary menu reflects chosen primary value + expect(page).to have_select(secondary_id_for('linked dropdown'), options: ['', 'Secondary 2.1', 'Secondary 2.2', 'Secondary 2.3']) + + # Select another primary value + select('Primary 1', from: primary_id_for('linked dropdown')) + + # Secondary menu gets updated + expect(page).to have_select(secondary_id_for('linked dropdown'), options: ['', 'Secondary 1.1', 'Secondary 1.2']) + end + + private + + def log_in(email, password, procedure) + visit "/commencer/#{procedure.procedure_path.path}" + expect(page).to have_current_path(new_user_session_path) + + fill_in 'user_email', with: email + fill_in 'user_password', with: password + click_on 'Se connecter' + expect(page).to have_current_path(identite_dossier_path(user_dossier)) + end + + def fill_individual + fill_in('individual_prenom', with: 'prenom') + fill_in('individual_nom', with: 'nom') + check 'dossier_autorisation_donnees' + click_on 'Continuer' + expect(page).to have_current_path(modifier_dossier_path(user_dossier)) + end + + def primary_id_for(libelle) + find(:xpath, ".//label[contains(text()[normalize-space()], '#{libelle}')]")[:for] + end + + def secondary_id_for(libelle) + primary_id = primary_id_for(libelle) + link = find("\##{primary_id}")['data-primary-id'] + find("[data-secondary-id=\"#{link}\"]")['id'] + end +end diff --git a/spec/models/champs/linked_drop_down_list_champ_spec.rb b/spec/models/champs/linked_drop_down_list_champ_spec.rb new file mode 100644 index 000000000..95327e5cb --- /dev/null +++ b/spec/models/champs/linked_drop_down_list_champ_spec.rb @@ -0,0 +1,18 @@ +require 'spec_helper' + +describe Champs::LinkedDropDownListChamp do + describe '#unpack_value' do + let(:champ) { described_class.new(value: '["tata", "tutu"]') } + + it { expect(champ.primary_value).to eq('tata') } + it { expect(champ.secondary_value).to eq('tutu') } + end + + describe '#pack_value' do + let(:champ) { described_class.new(primary_value: 'tata', secondary_value: 'tutu') } + + before { champ.save } + + it { expect(champ.value).to eq('["tata","tutu"]') } + end +end diff --git a/spec/models/types_de_champ/linked_drop_down_list_type_de_champ_spec.rb b/spec/models/types_de_champ/linked_drop_down_list_type_de_champ_spec.rb new file mode 100644 index 000000000..ab0fcf203 --- /dev/null +++ b/spec/models/types_de_champ/linked_drop_down_list_type_de_champ_spec.rb @@ -0,0 +1,40 @@ +require 'spec_helper' + +describe TypesDeChamp::LinkedDropDownListTypeDeChamp do + describe '#unpack_options' do + let(:drop_down_list) { build(:drop_down_list, value: menu_options) } + let(:type_de_champ) { described_class.new(drop_down_list: drop_down_list) } + + context 'with no options' do + let(:menu_options) { '' } + it { expect(type_de_champ.secondary_options).to eq({}) } + it { expect(type_de_champ.primary_options).to eq([]) } + end + + context 'with two primary options' do + let(:menu_options) do + <<~END_OPTIONS + --Primary 1-- + secondary 1.1 + secondary 1.2 + --Primary 2-- + secondary 2.1 + secondary 2.2 + secondary 2.3 + END_OPTIONS + end + + it do + expect(type_de_champ.secondary_options).to eq( + { + '' => [], + 'Primary 1' => [ '', 'secondary 1.1', 'secondary 1.2'], + 'Primary 2' => [ '', 'secondary 2.1', 'secondary 2.2', 'secondary 2.3'] + } + ) + end + + it { expect(type_de_champ.primary_options).to eq([ '', 'Primary 1', 'Primary 2' ]) } + end + end +end diff --git a/spec/views/layouts/_new_header_spec.rb b/spec/views/layouts/_new_header_spec.rb index 17ceb970b..49fd3345a 100644 --- a/spec/views/layouts/_new_header_spec.rb +++ b/spec/views/layouts/_new_header_spec.rb @@ -15,7 +15,7 @@ describe 'layouts/_new_header.html.haml', type: :view do let(:profile) { :user } it { is_expected.to have_css("a.header-logo[href=\"#{users_dossiers_path}\"]") } - it { is_expected.to have_link("Dossiers", href: dossiers_path) } + it { is_expected.to have_link("Dossiers", href: users_dossiers_path) } end context 'when rendering for gestionnaire' do