Merge pull request #8685 from demarches-simplifiees/prefill/communes

feat(dossier): prefill communes champ
This commit is contained in:
Sébastien Carceles 2023-02-27 11:28:49 +00:00 committed by GitHub
commit 36641d9ea7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 298 additions and 11 deletions

View file

@ -263,13 +263,14 @@ class TypeDeChamp < ApplicationRecord
TypeDeChamp.type_champs.fetch(:civilite),
TypeDeChamp.type_champs.fetch(:pays),
TypeDeChamp.type_champs.fetch(:regions),
TypeDeChamp.type_champs.fetch(:departements),
TypeDeChamp.type_champs.fetch(:communes),
TypeDeChamp.type_champs.fetch(:date),
TypeDeChamp.type_champs.fetch(:datetime),
TypeDeChamp.type_champs.fetch(:yes_no),
TypeDeChamp.type_champs.fetch(:checkbox),
TypeDeChamp.type_champs.fetch(:drop_down_list),
TypeDeChamp.type_champs.fetch(:repetition),
TypeDeChamp.type_champs.fetch(:departements),
TypeDeChamp.type_champs.fetch(:multiple_drop_down_list),
TypeDeChamp.type_champs.fetch(:epci)
])

View file

@ -0,0 +1,50 @@
class TypesDeChamp::PrefillCommuneTypeDeChamp < TypesDeChamp::PrefillTypeDeChamp
def possible_values
departements.map do |departement|
"#{departement[:code]} (#{departement[:name]}) : https://geo.api.gouv.fr/communes?codeDepartement=#{departement[:code]}"
end
end
def example_value
departement_code = departements.pick(:code)
commune_code = APIGeoService.communes(departement_code).pick(:code)
[departement_code, commune_code]
end
def to_assignable_attributes(champ, value)
return if value.blank? || !value.is_a?(Array)
return if (departement_code = value.first).blank?
return if (departement_name = APIGeoService.departement_name(departement_code)).blank?
return if !value.one? && (commune_code = value.second).blank?
return if !value.one? && (commune_name = APIGeoService.commune_name(departement_code, commune_code)).blank?
if value.one?
departement_attributes(champ, departement_code, departement_name)
else
departement_and_commune_attributes(champ, departement_code, departement_name, commune_code, commune_name)
end
end
private
def departement_attributes(champ, departement_code, departement_name)
{
id: champ.id,
code_departement: departement_code,
departement: departement_name
}
end
def departement_and_commune_attributes(champ, departement_code, departement_name, commune_code, commune_name)
postal_code = APIGeoService.commune_postal_codes(departement_code, commune_code).first
departement_attributes(champ, departement_code, departement_name).merge(
external_id: commune_code,
value: "#{commune_name} (#{postal_code})"
)
end
def departements
@departements ||= APIGeoService.departements.sort_by { _1[:code] }
end
end

View file

@ -23,6 +23,8 @@ class TypesDeChamp::PrefillTypeDeChamp < SimpleDelegator
TypesDeChamp::PrefillRepetitionTypeDeChamp.new(type_de_champ, revision)
when TypeDeChamp.type_champs.fetch(:departements)
TypesDeChamp::PrefillDepartementTypeDeChamp.new(type_de_champ, revision)
when TypeDeChamp.type_champs.fetch(:communes)
TypesDeChamp::PrefillCommuneTypeDeChamp.new(type_de_champ, revision)
when TypeDeChamp.type_champs.fetch(:epci)
TypesDeChamp::PrefillEpciTypeDeChamp.new(type_de_champ, revision)
else

View file

@ -47,6 +47,25 @@ class APIGeoService
departements.find { _1[:name] == name }&.dig(:code)
end
def communes(departement_code)
get_from_api_geo(
"communes?codeDepartement=#{departement_code}",
additional_keys: { postal_codes: :codesPostaux }
).sort_by { I18n.transliterate(_1[:name]) }
end
def commune_name(departement_code, code)
communes(departement_code).find { _1[:code] == code }&.dig(:name)
end
def commune_code(departement_code, name)
communes(departement_code).find { _1[:name] == name }&.dig(:code)
end
def commune_postal_codes(departement_code, code)
communes(departement_code).find { _1[:code] == code }&.dig(:postal_codes)
end
def epcis(departement_code)
get_from_api_geo("epcis?codeDepartement=#{departement_code}").sort_by { I18n.transliterate(_1[:name]) }
end
@ -61,11 +80,16 @@ class APIGeoService
private
def get_from_api_geo(scope)
def get_from_api_geo(scope, additional_keys: {})
Rails.cache.fetch("api_geo_#{scope}", expires_in: 1.year) do
response = Typhoeus.get("#{API_GEO_URL}/#{scope}")
JSON.parse(response.body).map(&:symbolize_keys)
.map { { name: _1[:nom].tr("'", ''), code: _1[:code] } }
JSON.parse(response.body)
.map(&:symbolize_keys)
.map do |result|
data = { name: result[:nom].tr("'", ''), code: result[:code] }
additional_keys.each { |key, value| data = data.merge(key => result[value]) }
data
end
end
end

View file

@ -129,14 +129,14 @@ en:
yes_no_html: '"true" for Yes, "false" pour No'
checkbox_html: '"true" to check, "false" to uncheck'
pays_html: An <a href="https://en.wikipedia.org/wiki/ISO_3166-2" target="_blank" rel="noopener noreferrer">ISO 3166-2 country code</a>
regions_html: An <a href="https://fr.wikipedia.org/wiki/R%C3%A9gion_fran%C3%A7aise" target="_blank" rel="noopener noreferrer">INSEE region code</a>
departements_html: A <a href="https://fr.wikipedia.org/wiki/Num%C3%A9rotation_des_d%C3%A9partements_fran%C3%A7ais" target="_blank">department number</a>
communes_html: An array of the department code and the <a href="https://geo.api.gouv.fr/communes" target="_blank" rel="noopener noreferrer">INSEE commune code</a>.
drop_down_list_html: A choice among those selected when the procedure was created
date_html: ISO8601 date
datetime_html: ISO8601 datetime
drop_down_list_other_html: Any value
repetition_html: A array of hashes with possible values for each field of the repetition.
regions_html: An <a href="https://fr.wikipedia.org/wiki/R%C3%A9gion_fran%C3%A7aise" target="_blank" rel="noopener noreferrer">INSEE region code</a>
regions_html: An <a href="https://fr.wikipedia.org/wiki/R%C3%A9gion_fran%C3%A7aise" target="_blank" rel="noopener noreferrer">INSEE region code</a>
epci_html: An array of the department code and the <a href="https://geo.api.gouv.fr/epcis" target="_blank" rel="noopener noreferrer">EPCI one</a>.
examples:
title: Example

View file

@ -120,13 +120,14 @@ fr:
yes_no_html: '"true" pour Oui, "false" pour Non'
checkbox_html: '"true" pour coché, "false" pour décoché'
pays_html: Un <a href="https://en.wikipedia.org/wiki/ISO_3166-2" target="_blank" rel="noopener noreferrer">code pays ISO 3166-2</a>
regions_html: Un <a href="https://fr.wikipedia.org/wiki/R%C3%A9gion_fran%C3%A7aise" target="_blank" rel="noopener noreferrer">code INSEE de région</a>
departements_html: Un <a href="https://fr.wikipedia.org/wiki/Num%C3%A9rotation_des_d%C3%A9partements_fran%C3%A7ais" target="_blank">numéro de département</a>
communes_html: Un tableau contenant le code de département et <a href="https://geo.api.gouv.fr/communes" target="_blank" rel="noopener noreferrer">le code INSEE de la commune</a>.
drop_down_list_html: Un choix parmi ceux sélectionnés à la création de la procédure
datetime_html: Datetime au format ISO8601
date_html: Date au format ISO8601
drop_down_list_other_html: Toute valeur
repetition_html: Un tableau de dictionnaires avec les valeurs possibles pour chaque champ de la répétition.
regions_html: Un <a href="https://fr.wikipedia.org/wiki/R%C3%A9gion_fran%C3%A7aise" target="_blank" rel="noopener noreferrer">code INSEE de région</a>
epci_html: Un tableau contenant le code de département et <a href="https://geo.api.gouv.fr/epcis" target="_blank" rel="noopener noreferrer">celui de l'EPCI</a>.
examples:
title: Exemple

File diff suppressed because one or more lines are too long

View file

@ -15,12 +15,14 @@ RSpec.describe PrefillParams do
VCR.insert_cassette('api_geo_regions')
VCR.insert_cassette('api_geo_departements')
VCR.insert_cassette('api_geo_communes')
VCR.insert_cassette('api_geo_epcis')
end
after do
VCR.eject_cassette('api_geo_regions')
VCR.eject_cassette('api_geo_departements')
VCR.eject_cassette('api_geo_communes')
VCR.eject_cassette('api_geo_epcis')
end
@ -137,6 +139,7 @@ RSpec.describe PrefillParams do
it_behaves_like "a champ public value that is authorized", :checkbox, "false"
it_behaves_like "a champ public value that is authorized", :drop_down_list, "value"
it_behaves_like "a champ public value that is authorized", :departements, "03"
it_behaves_like "a champ public value that is authorized", :communes, ['01', '01457']
it_behaves_like "a champ public value that is authorized", :multiple_drop_down_list, ["val1", "val2"]
it_behaves_like "a champ public value that is authorized", :epci, ['01', '200042935']
@ -173,6 +176,7 @@ RSpec.describe PrefillParams do
it_behaves_like "a champ private value that is authorized", :drop_down_list, "value"
it_behaves_like "a champ private value that is authorized", :regions, "93"
it_behaves_like "a champ private value that is authorized", :departements, "03"
it_behaves_like "a champ private value that is authorized", :communes, ['01', '01457']
it_behaves_like "a champ private value that is authorized", :multiple_drop_down_list, ["val1", "val2"]
it_behaves_like "a champ private value that is authorized", :epci, ['01', '200042935']
@ -193,7 +197,6 @@ RSpec.describe PrefillParams do
it_behaves_like "a champ public value that is unauthorized", :decimal_number, "non decimal string"
it_behaves_like "a champ public value that is unauthorized", :integer_number, "non integer string"
it_behaves_like "a champ public value that is unauthorized", :number, "value"
it_behaves_like "a champ public value that is unauthorized", :communes, "value"
it_behaves_like "a champ public value that is unauthorized", :dossier_link, "value"
it_behaves_like "a champ public value that is unauthorized", :titre_identite, "value"
it_behaves_like "a champ public value that is unauthorized", :civilite, "value"
@ -213,6 +216,7 @@ RSpec.describe PrefillParams do
it_behaves_like "a champ public value that is unauthorized", :pays, "value"
it_behaves_like "a champ public value that is unauthorized", :regions, "value"
it_behaves_like "a champ public value that is unauthorized", :departements, "value"
it_behaves_like "a champ public value that is unauthorized", :communes, "value"
it_behaves_like "a champ public value that is unauthorized", :siret, "value"
it_behaves_like "a champ public value that is unauthorized", :rna, "value"
it_behaves_like "a champ public value that is unauthorized", :annuaire_education, "value"

View file

@ -246,16 +246,17 @@ describe TypeDeChamp do
it_behaves_like "a prefillable type de champ", :type_de_champ_datetime
it_behaves_like "a prefillable type de champ", :type_de_champ_civilite
it_behaves_like "a prefillable type de champ", :type_de_champ_pays
it_behaves_like "a prefillable type de champ", :type_de_champ_regions
it_behaves_like "a prefillable type de champ", :type_de_champ_departements
it_behaves_like "a prefillable type de champ", :type_de_champ_communes
it_behaves_like "a prefillable type de champ", :type_de_champ_yes_no
it_behaves_like "a prefillable type de champ", :type_de_champ_checkbox
it_behaves_like "a prefillable type de champ", :type_de_champ_drop_down_list
it_behaves_like "a prefillable type de champ", :type_de_champ_repetition
it_behaves_like "a prefillable type de champ", :type_de_champ_departements
it_behaves_like "a prefillable type de champ", :type_de_champ_multiple_drop_down_list
it_behaves_like "a prefillable type de champ", :type_de_champ_epci
it_behaves_like "a non-prefillable type de champ", :type_de_champ_number
it_behaves_like "a non-prefillable type de champ", :type_de_champ_communes
it_behaves_like "a non-prefillable type de champ", :type_de_champ_dossier_link
it_behaves_like "a non-prefillable type de champ", :type_de_champ_titre_identite
it_behaves_like "a non-prefillable type de champ", :type_de_champ_linked_drop_down_list

View file

@ -0,0 +1,123 @@
# frozen_string_literal: true
RSpec.describe TypesDeChamp::PrefillCommuneTypeDeChamp do
let(:procedure) { create(:procedure) }
let(:type_de_champ) { build(:type_de_champ_communes, procedure: procedure) }
let(:memory_store) { ActiveSupport::Cache.lookup_store(:memory_store) }
before do
allow(Rails).to receive(:cache).and_return(memory_store)
Rails.cache.clear
end
before do
VCR.insert_cassette('api_geo_departements')
VCR.insert_cassette('api_geo_communes')
end
after do
VCR.eject_cassette('api_geo_departements')
VCR.eject_cassette('api_geo_communes')
end
describe 'ancestors' do
subject { described_class.new(type_de_champ, procedure.active_revision) }
it { is_expected.to be_kind_of(TypesDeChamp::PrefillTypeDeChamp) }
end
describe '#possible_values' do
let(:expected_values) do
departements.map { |departement| "#{departement[:code]} (#{departement[:name]}) : https://geo.api.gouv.fr/communes?codeDepartement=#{departement[:code]}" }
end
subject(:possible_values) { described_class.new(type_de_champ, procedure.active_revision).possible_values }
it { expect(possible_values).to match(expected_values) }
end
describe '#example_value' do
let(:departement_code) { departements.pick(:code) }
let(:commune_code) { APIGeoService.communes(departement_code).pick(:code) }
subject(:example_value) { described_class.new(type_de_champ, procedure.active_revision).example_value }
it { is_expected.to eq([departement_code, commune_code]) }
end
describe '#to_assignable_attributes' do
let(:champ) { create(:champ_communes, type_de_champ: type_de_champ) }
subject(:to_assignable_attributes) do
described_class.build(type_de_champ, procedure.active_revision).to_assignable_attributes(champ, value)
end
context 'when the value is nil' do
let(:value) { nil }
it { is_expected.to match(nil) }
end
context 'when the value is empty' do
let(:value) { '' }
it { is_expected.to match(nil) }
end
context 'when the value is a string' do
let(:value) { 'hello' }
it { is_expected.to match(nil) }
end
context 'when the value is an array of one element' do
context 'when the first element is a valid departement code' do
let(:value) { ['01'] }
it { is_expected.to match({ id: champ.id, code_departement: '01', departement: 'Ain' }) }
end
context 'when the first element is not a valid departement code' do
let(:value) { ['totoro'] }
it { is_expected.to match(nil) }
end
end
context 'when the value is an array of two elements' do
context 'when the first element is a valid departement code' do
context 'when the second element is a valid insee code' do
let(:value) { ['01', '01457'] }
it { is_expected.to match({ id: champ.id, code_departement: '01', departement: 'Ain', external_id: '01457', value: 'Vonnas (01540)' }) }
end
context 'when the second element is not a valid insee code' do
let(:value) { ['01', 'totoro'] }
it { is_expected.to match(nil) }
end
end
context 'when the first element is not a valid departement code' do
let(:value) { ['totoro', '01457'] }
it { is_expected.to match(nil) }
end
end
context 'when the value is an array of three or more elements' do
context 'when the first element is a valid departement code' do
context 'when the second element is a valid insee code' do
let(:value) { ['01', '01457', 'hello'] }
it { is_expected.to match({ id: champ.id, code_departement: '01', departement: 'Ain', external_id: '01457', value: 'Vonnas (01540)' }) }
end
context 'when the second element is not a valid insee code' do
let(:value) { ['01', 'totoro', 'hello'] }
it { is_expected.to match(nil) }
end
end
context 'when the first element is not a valid departement code' do
let(:value) { ['totoro', '01457', 'hello'] }
it { is_expected.to match(nil) }
end
end
end
private
def departements
APIGeoService.departements.sort_by { _1[:code] }
end
end

View file

@ -45,6 +45,12 @@ RSpec.describe TypesDeChamp::PrefillTypeDeChamp, type: :model do
it { expect(built).to be_kind_of(TypesDeChamp::PrefillDepartementTypeDeChamp) }
end
context 'when the type de champ is a communes' do
let(:type_de_champ) { build(:type_de_champ_communes) }
it { expect(built).to be_kind_of(TypesDeChamp::PrefillCommuneTypeDeChamp) }
end
context 'when the type de champ is a epci' do
let(:type_de_champ) { build(:type_de_champ_epci, procedure: procedure) }

View file

@ -43,6 +43,29 @@ describe APIGeoService do
end
end
describe 'communes', vcr: { cassette_name: 'api_geo_communes' } do
it 'return sorted results' do
expect(APIGeoService.communes('01').size).to eq(393)
expect(APIGeoService.communes('01').first).to eq(code: '01004', name: 'Ambérieu-en-Bugey', postal_codes: ['01500'])
expect(APIGeoService.communes('01').last).to eq(code: '01457', name: 'Vonnas', postal_codes: ['01540'])
end
end
describe 'commune_name', vcr: { cassette_name: 'api_geo_communes' } do
subject { APIGeoService.commune_name('01', '01457') }
it { is_expected.to eq('Vonnas') }
end
describe 'commune_code', vcr: { cassette_name: 'api_geo_communes' } do
subject { APIGeoService.commune_code('01', 'Vonnas') }
it { is_expected.to eq('01457') }
end
describe 'commune_postal_codes', vcr: { cassette_name: 'api_geo_communes' } do
subject { APIGeoService.commune_postal_codes('01', '01457') }
it { is_expected.to eq(['01540']) }
end
describe 'epcis', vcr: { cassette_name: 'api_geo_epcis' } do
it 'return sorted results' do
expect(APIGeoService.epcis('01').size).to eq(17)

View file

@ -26,5 +26,6 @@ shared_examples "the user has got a prefilled dossier, owned by themselves" do
expect(page).to have_content(multiple_drop_down_list_values.first)
expect(page).to have_content(multiple_drop_down_list_values.last)
expect(page).to have_field(type_de_champ_epci.libelle, with: epci_value.last)
expect(page).to have_selector("input[value='Vonnas (01540)']")
end
end

View file

@ -11,6 +11,7 @@ describe 'Prefilling a dossier (with a GET request):' do
let(:type_de_champ_datetime) { create(:type_de_champ_datetime, procedure: procedure) }
let(:type_de_champ_multiple_drop_down_list) { create(:type_de_champ_multiple_drop_down_list, procedure: procedure) }
let(:type_de_champ_epci) { create(:type_de_champ_epci, procedure: procedure) }
let(:type_de_champ_commune) { create(:type_de_champ_communes, procedure: procedure) }
let(:type_de_champ_repetition) { create(:type_de_champ_repetition, :with_types_de_champ, procedure: procedure) }
let(:text_value) { "My Neighbor Totoro is the best movie ever" }
let(:phone_value) { "invalid phone value" }
@ -22,6 +23,7 @@ describe 'Prefilling a dossier (with a GET request):' do
]
}
let(:epci_value) { ['01', '200029999'] }
let(:commune_value) { ['01', '01457'] } # Vonnas (01540)
let(:sub_type_de_champs_repetition) { procedure.active_revision.children_of(type_de_champ_repetition) }
let(:text_repetition_libelle) { sub_type_de_champs_repetition.first.libelle }
let(:integer_repetition_libelle) { sub_type_de_champs_repetition.second.libelle }
@ -36,6 +38,7 @@ describe 'Prefilling a dossier (with a GET request):' do
"champ_#{type_de_champ_datetime.to_typed_id_for_query}" => datetime_value,
"champ_#{type_de_champ_multiple_drop_down_list.to_typed_id_for_query}" => multiple_drop_down_list_values,
"champ_#{type_de_champ_epci.to_typed_id_for_query}" => epci_value,
"champ_#{type_de_champ_commune.to_typed_id_for_query}" => commune_value,
"champ_#{type_de_champ_repetition.to_typed_id_for_query}" => [
{
"champ_#{sub_type_de_champs_repetition.first.to_typed_id_for_query}": text_repetition_value,
@ -50,11 +53,13 @@ describe 'Prefilling a dossier (with a GET request):' do
Rails.cache.clear
VCR.insert_cassette('api_geo_departements')
VCR.insert_cassette('api_geo_communes')
VCR.insert_cassette('api_geo_epcis')
end
after do
VCR.eject_cassette('api_geo_departements')
VCR.eject_cassette('api_geo_communes')
VCR.eject_cassette('api_geo_epcis')
end

View file

@ -12,6 +12,7 @@ describe 'Prefilling a dossier (with a POST request):' do
let(:type_de_champ_multiple_drop_down_list) { create(:type_de_champ_multiple_drop_down_list, procedure: procedure) }
let(:type_de_champ_epci) { create(:type_de_champ_epci, procedure: procedure) }
let(:type_de_champ_repetition) { create(:type_de_champ_repetition, :with_types_de_champ, procedure: procedure) }
let(:type_de_champ_commune) { create(:type_de_champ_communes, procedure: procedure) }
let(:text_value) { "My Neighbor Totoro is the best movie ever" }
let(:phone_value) { "invalid phone value" }
let(:datetime_value) { "2023-02-01T10:32" }
@ -22,6 +23,7 @@ describe 'Prefilling a dossier (with a POST request):' do
]
}
let(:epci_value) { ['01', '200029999'] }
let(:commune_value) { ['01', '01457'] } # Vonnas (01540)
let(:sub_type_de_champs_repetition) { procedure.active_revision.children_of(type_de_champ_repetition) }
let(:text_repetition_libelle) { sub_type_de_champs_repetition.first.libelle }
let(:integer_repetition_libelle) { sub_type_de_champs_repetition.second.libelle }
@ -33,11 +35,13 @@ describe 'Prefilling a dossier (with a POST request):' do
Rails.cache.clear
VCR.insert_cassette('api_geo_departements')
VCR.insert_cassette('api_geo_communes')
VCR.insert_cassette('api_geo_epcis')
end
after do
VCR.eject_cassette('api_geo_departements')
VCR.eject_cassette('api_geo_communes')
VCR.eject_cassette('api_geo_epcis')
end
@ -136,7 +140,8 @@ describe 'Prefilling a dossier (with a POST request):' do
],
"champ_#{type_de_champ_datetime.to_typed_id_for_query}" => datetime_value,
"champ_#{type_de_champ_multiple_drop_down_list.to_typed_id_for_query}" => multiple_drop_down_list_values,
"champ_#{type_de_champ_epci.to_typed_id_for_query}" => epci_value
"champ_#{type_de_champ_epci.to_typed_id_for_query}" => epci_value,
"champ_#{type_de_champ_commune.to_typed_id_for_query}" => commune_value
}.to_json
JSON.parse(session.response.body)["dossier_url"].gsub("http://www.example.com", "")
end