Merge pull request #8783 from tchak/improuve-champ-commune

refactor(commune): choisir la commune par son code postal
This commit is contained in:
Paul Chavard 2023-03-29 13:51:06 +00:00 committed by GitHub
commit acc8584cdf
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
29 changed files with 735 additions and 365 deletions

View file

@ -1,2 +1,17 @@
class EditableChamp::CommunesComponent < EditableChamp::ComboSearchComponent class EditableChamp::CommunesComponent < EditableChamp::EditableChampBaseComponent
include ApplicationHelper
private
def commune_options
@champ.communes.map { ["#{_1[:name]} (#{_1[:postal_code]})", _1[:code]] }
end
def code_postal_input_id
"#{@champ.input_id}-code_postal"
end
def commune_select_options
{ selected: @champ.selected }.merge(@champ.mandatory? ? { prompt: '' } : { include_blank: '' })
end
end end

View file

@ -0,0 +1,4 @@
---
en:
postal_code: Postal code of the municipality
not_found: No municipality found for postal code %{postal_code}

View file

@ -0,0 +1,4 @@
---
fr:
postal_code: Code postal de la commune
not_found: Aucune commune trouvée pour le code postal %{postal_code}

View file

@ -1,12 +1,13 @@
- render_parent %label.notice{ for: code_postal_input_id }= t('.postal_code')
= @form.hidden_field :value = @form.text_field :code_postal, required: @champ.required?, id: code_postal_input_id, class: "width-33-desktop width-100-mobile small-margin"
= @form.hidden_field :external_id - if @champ.code_postal_with_fallback?
= @form.hidden_field :departement - if commune_options.empty?
= @form.hidden_field :code_departement .fr-error-text.mb-4= t('.not_found', postal_code: @champ.code_postal_with_fallback)
= react_component("ComboCommunesSearch", - elsif commune_options.size <= 3
required: @champ.required?, %fieldset.radios
id: @champ.input_id, - commune_options.each.with_index do |(option, value), index|
classNameDepartement: "width-33-desktop width-100-mobile", %label
className: "width-66-desktop width-100-mobile", = @form.radio_button :value, value, checked: @champ.selected == value, id: index == 0 ? @champ.input_id : nil
describedby: @champ.describedby_id, = option
**react_combo_props) - else
= @form.select :value, commune_options, commune_select_options, required: @champ.required?, id: @champ.input_id, aria: { describedby: @champ.describedby_id }, class: "width-33-desktop width-100-mobile"

View file

@ -326,8 +326,8 @@ module Instructeurs
def champs_private_params def champs_private_params
champs_params = params.require(:dossier).permit(champs_private_attributes: [ champs_params = params.require(:dossier).permit(champs_private_attributes: [
:id, :primary_value, :secondary_value, :piece_justificative_file, :value_other, :external_id, :numero_allocataire, :code_postal, :departement, :code_departement, :value, value: [], :id, :primary_value, :secondary_value, :piece_justificative_file, :value_other, :external_id, :numero_allocataire, :code_postal, :code_departement, :value, value: [],
champs_attributes: [:id, :_destroy, :value, :primary_value, :secondary_value, :piece_justificative_file, :value_other, :external_id, :numero_allocataire, :code_postal, :departement, :code_departement, value: []] champs_attributes: [:id, :_destroy, :value, :primary_value, :secondary_value, :piece_justificative_file, :value_other, :external_id, :numero_allocataire, :code_postal, :code_departement, value: []]
]) ])
champs_params[:champs_private_all_attributes] = champs_params.delete(:champs_private_attributes) || {} champs_params[:champs_private_all_attributes] = champs_params.delete(:champs_private_attributes) || {}
champs_params champs_params

View file

@ -387,7 +387,7 @@ module Users
def champs_public_params def champs_public_params
champs_params = params.require(:dossier).permit(champs_public_attributes: [ champs_params = params.require(:dossier).permit(champs_public_attributes: [
:id, :value, :value_other, :external_id, :primary_value, :secondary_value, :numero_allocataire, :code_postal, :identifiant, :numero_fiscal, :reference_avis, :ine, :piece_justificative_file, :departement, :code_departement, value: [], :id, :value, :value_other, :external_id, :primary_value, :secondary_value, :numero_allocataire, :code_postal, :identifiant, :numero_fiscal, :reference_avis, :ine, :piece_justificative_file, :code_departement, value: [],
champs_attributes: [:id, :_destroy, :value, :value_other, :external_id, :primary_value, :secondary_value, :numero_allocataire, :code_postal, :identifiant, :numero_fiscal, :reference_avis, :ine, :piece_justificative_file, :departement, :code_departement, value: []] champs_attributes: [:id, :_destroy, :value, :value_other, :external_id, :primary_value, :secondary_value, :numero_allocataire, :code_postal, :identifiant, :numero_fiscal, :reference_avis, :ine, :piece_justificative_file, :departement, :code_departement, value: []]
]) ])
champs_params[:champs_public_all_attributes] = champs_params.delete(:champs_public_attributes) || {} champs_params[:champs_public_all_attributes] = champs_params.delete(:champs_public_attributes) || {}

View file

@ -404,7 +404,16 @@ type Commune {
Le code INSEE Le code INSEE
""" """
code: String! code: String!
"""
Le nom de la commune
"""
name: String! name: String!
"""
Le code postal
"""
postalCode: String
} }
type CommuneChamp implements Champ { type CommuneChamp implements Champ {

View file

@ -3,29 +3,20 @@ module Types::Champs
implements Types::ChampType implements Types::ChampType
class CommuneType < Types::BaseObject class CommuneType < Types::BaseObject
field :name, String, null: false field :name, String, "Le nom de la commune", null: false
field :code, String, "Le code INSEE", null: false field :code, String, "Le code INSEE", null: false
field :postal_code, String, "Le code postal", null: true, method: :code_postal
end end
field :commune, CommuneType, null: true field :commune, CommuneType, null: true
field :departement, Types::Champs::DepartementChampType::DepartementType, null: true field :departement, Types::Champs::DepartementChampType::DepartementType, null: true
def commune def commune
if object.code? object if object.code?
{
name: object.to_s,
code: object.code
}
end
end end
def departement def departement
if object.departement? object.departement if object.departement?
{
name: object.name_departement,
code: object.code_departement
}
end
end end
end end
end end

View file

@ -1,124 +0,0 @@
import React from 'react';
import { QueryClientProvider } from 'react-query';
import { matchSorter } from 'match-sorter';
import ComboSearch, { ComboSearchProps } from './ComboSearch';
import { queryClient } from './shared/queryClient';
import { ComboDepartementsSearch } from './ComboDepartementsSearch';
import { useHiddenField, groupId } from './shared/hooks';
type CommuneResult = { code: string; nom: string; codesPostaux: string[] };
// Avoid hiding similar matches for precise queries (like "Sainte Marie")
function searchResultsLimit(term: string) {
return term.length > 5 ? 10 : 5;
}
function expandResultsWithMultiplePostalCodes(term: string, result: unknown) {
const results = result as CommuneResult[];
// A single result may have several associated postal codes.
// To make the search results more precise, we want to generate
// an actual result for each postal code.
const expandedResults = results.flatMap((result) =>
result.codesPostaux.map((codePostal) => ({
...result,
codesPostaux: [codePostal]
}))
);
// Some very large cities (like Paris) have A LOT of associated postal codes.
// As we generated one result per postal code, we now have a lot of results
// for the same city. If the number of results is above the threshold, we use
// local search to narrow the results.
const limit = searchResultsLimit(term);
if (expandedResults.length > limit) {
return matchSorter(expandedResults, term, {
keys: [(item) => `${item.nom} (${item.codesPostaux[0]})`, 'code'],
sorter: (rankedItems) => rankedItems
}).slice(0, limit + 1);
}
return expandedResults;
}
const placeholderDepartements = [
['63 Puy-de-Dôme', 'Clermont-Ferrand'],
['77 Seine-et-Marne', 'Melun'],
['22 Côtes dArmor', 'Saint-Brieuc'],
['47 Lot-et-Garonne', 'Agen']
] as const;
const [placeholderDepartement, placeholderCommune] =
placeholderDepartements[
Math.floor(Math.random() * (placeholderDepartements.length - 1))
];
export default function ComboCommunesSearch({
id,
classNameDepartement,
...props
}: ComboSearchProps<CommuneResult> & {
id: string;
classNameDepartement?: string;
}) {
const group = groupId(id);
const [departementValue, setDepartementValue] = useHiddenField(
group,
'departement'
);
const [codeDepartement, setCodeDepartement] = useHiddenField(
group,
'code_departement'
);
const departementDescribedBy = `${id}_departement_notice`;
const communeDescribedBy = `${id}_commune_notice`;
return (
<QueryClientProvider client={queryClient}>
<div>
<div className="notice" id={departementDescribedBy}>
<p>
Choisissez le département dans lequel se situe la commune. Vous
pouvez entrer le nom ou le code.
</p>
</div>
<ComboDepartementsSearch
{...props}
id={!codeDepartement ? id : undefined}
describedby={departementDescribedBy}
placeholder={placeholderDepartement}
addForeignDepartement={false}
value={departementValue}
className={classNameDepartement}
onChange={(_, result) => {
setDepartementValue(result?.nom ?? '');
setCodeDepartement(result?.code ?? '');
}}
/>
</div>
{codeDepartement ? (
<div>
<div className="notice" id={communeDescribedBy}>
<p>
Choisissez la commune. Vous pouvez entrer le nom ou le code
postal.
</p>
</div>
<ComboSearch
{...props}
id={id}
describedby={communeDescribedBy}
placeholder={placeholderCommune}
scope="communes"
scopeExtra={codeDepartement}
minimumInputLength={2}
transformResult={({ code, nom, codesPostaux }) => [
code,
`${nom} (${codesPostaux[0]})`
]}
transformResults={expandResultsWithMultiplePostalCodes}
/>
</div>
) : null}
</QueryClientProvider>
);
}

View file

@ -1,42 +0,0 @@
import React from 'react';
import { matchSorter } from 'match-sorter';
import ComboSearch, { ComboSearchProps } from './ComboSearch';
type DepartementResult = { code: string; nom: string };
const extraTerms = [{ code: '99', nom: 'Etranger' }];
function expandResultsWithForeignDepartement(term: string, result: unknown) {
const results = result as DepartementResult[];
return [
...results,
...matchSorter(extraTerms, term, {
keys: ['nom', 'code']
})
];
}
type ComboDepartementsSearchProps = Omit<
ComboSearchProps<DepartementResult> & {
addForeignDepartement: boolean;
},
'transformResult' | 'transformResults'
>;
export function ComboDepartementsSearch({
addForeignDepartement = true,
...props
}: ComboDepartementsSearchProps) {
return (
<ComboSearch
{...props}
scope="departements"
minimumInputLength={1}
transformResult={({ code, nom }) => [code, `${code} - ${nom}`]}
transformResults={
addForeignDepartement ? expandResultsWithForeignDepartement : undefined
}
/>
);
}

View file

@ -1,22 +1,11 @@
import { QueryClient, QueryFunction } from 'react-query'; import { QueryClient, QueryFunction } from 'react-query';
import { httpRequest, isNumeric, getConfig } from '@utils'; import { httpRequest, getConfig } from '@utils';
import { matchSorter } from 'match-sorter';
const API_EDUCATION_QUERY_LIMIT = 5; const API_EDUCATION_QUERY_LIMIT = 5;
const API_GEO_QUERY_LIMIT = 5;
const API_ADRESSE_QUERY_LIMIT = 5; const API_ADRESSE_QUERY_LIMIT = 5;
// When searching for short strings like "mer", le exact match shows up quite far in
// the ordering (~50).
//
// That's why we deliberately fetch a lot of results, and then let the local matching
// (match-sorter) do the work.
//
// NB: 60 is arbitrary, we may add more if needed.
const API_GEO_COMMUNES_QUERY_LIMIT = 60;
const { const {
autocomplete: { api_geo_url, api_adresse_url, api_education_url } autocomplete: { api_adresse_url, api_education_url }
} = getConfig(); } = getConfig();
type QueryKey = readonly [ type QueryKey = readonly [
@ -25,10 +14,10 @@ type QueryKey = readonly [
extra: string | undefined extra: string | undefined
]; ];
function buildURL(scope: string, term: string, extra?: string) { function buildURL(scope: string, term: string) {
term = term.replace(/\(|\)/g, ''); term = term.replace(/\(|\)/g, '');
const params = new URLSearchParams(); const params = new URLSearchParams();
let path = `${api_geo_url}/${scope}`; let path = '';
if (scope == 'adresse') { if (scope == 'adresse') {
path = `${api_adresse_url}/search`; path = `${api_adresse_url}/search`;
@ -39,40 +28,15 @@ function buildURL(scope: string, term: string, extra?: string) {
params.set('q', term); params.set('q', term);
params.set('rows', `${API_EDUCATION_QUERY_LIMIT}`); params.set('rows', `${API_EDUCATION_QUERY_LIMIT}`);
params.set('dataset', 'fr-en-annuaire-education'); params.set('dataset', 'fr-en-annuaire-education');
} else if (scope == 'communes') {
if (extra) {
params.set('codeDepartement', extra);
}
if (isNumeric(term)) {
params.set('codePostal', term);
} else {
params.set('nom', term);
params.set('boost', 'population');
}
params.set('limit', `${API_GEO_COMMUNES_QUERY_LIMIT}`);
} else {
if (isNumeric(term)) {
params.set('code', term.padStart(2, '0'));
} else {
params.set('nom', term);
}
if (scope == 'departements') {
params.set('zone', 'metro,drom,com');
}
params.set('limit', `${API_GEO_QUERY_LIMIT}`);
} }
return `${path}?${params}`; return `${path}?${params}`;
} }
const defaultQueryFn: QueryFunction<unknown, QueryKey> = async ({ const defaultQueryFn: QueryFunction<unknown, QueryKey> = async ({
queryKey: [scope, term, extra], queryKey: [scope, term],
signal signal
}) => { }) => {
if (scope == 'pays') {
return matchSorter(await getPays(signal), term, { keys: ['label'] });
}
// BAN will error with queries less then 3 chars long // BAN will error with queries less then 3 chars long
if (scope == 'adresse' && term.length < 3) { if (scope == 'adresse' && term.length < 3) {
return { return {
@ -83,23 +47,10 @@ const defaultQueryFn: QueryFunction<unknown, QueryKey> = async ({
}; };
} }
const url = buildURL(scope, term, extra); const url = buildURL(scope, term);
return httpRequest(url, { csrf: false, signal }).json(); return httpRequest(url, { csrf: false, signal }).json();
}; };
let paysCache: { label: string }[];
async function getPays(signal?: AbortSignal): Promise<{ label: string }[]> {
if (!paysCache) {
const data = await httpRequest('/api/pays', { signal }).json<
typeof paysCache
>();
if (data) {
paysCache = data;
}
}
return paysCache;
}
export const queryClient = new QueryClient({ export const queryClient = new QueryClient({
defaultOptions: { defaultOptions: {
queries: { queries: {

View file

@ -21,30 +21,110 @@
# type_de_champ_id :integer # type_de_champ_id :integer
# #
class Champs::CommuneChamp < Champs::TextChamp class Champs::CommuneChamp < Champs::TextChamp
store_accessor :value_json, :departement, :code_departement store_accessor :value_json, :code_departement, :code_postal
before_validation :on_code_postal_change
def for_export def for_export
[value, external_id, departement? ? departement_code_and_name : ''] [name, code, departement? ? departement_code_and_name : '']
end end
def name_departement def departement_name
# FIXME we originaly saved already formatted departement with the code in the name APIGeoService.departement_name(code_departement)
departement&.gsub(/^(.[0-9])\s-\s/, '')
end end
def departement_code_and_name def departement_code_and_name
"#{code_departement} - #{name_departement}" "#{code_departement} #{departement_name}"
end
def departement
{ code: code_departement, name: departement_name }
end end
def departement? def departement?
departement.present? code_departement.present?
end end
def code? def code?
code.present? code.present?
end end
def code_postal?
code_postal.present?
end
def name
if code?
"#{APIGeoService.commune_name(code_departement, code)} (#{code_postal_with_fallback})"
else
value
end
end
def to_s
name
end
def code def code
external_id external_id
end end
def selected
code
end
def communes
if code_postal_with_fallback?
APIGeoService.communes_by_postal_code(code_postal_with_fallback)
else
[]
end
end
def value=(code)
if code.blank? || !code_postal_with_fallback?
self.code_departement = nil
self.external_id = nil
super(nil)
else
commune = communes.find { _1[:code] == code }
if commune.present?
self.code_departement = commune[:departement_code]
self.external_id = commune[:code]
super(commune[:name])
else
self.code_departement = nil
self.external_id = nil
super(nil)
end
end
end
def code_postal_with_fallback?
code_postal_with_fallback.present?
end
# We try to extract the postal code from the value, which is the name of the commune and the
# postal code in brackets. This is temporary until we do a full data migration.
def code_postal_with_fallback
if code_postal?
code_postal
elsif value.present?
match = value.match(/[^(]\(([^\)]*)\)$/)
match[1] if match.present?
else
nil
end
end
private
def on_code_postal_change
if code_postal_changed?
if communes.one?
self.value = communes.first[:code]
else
self.value = nil
end
end
end
end end

View file

@ -514,7 +514,7 @@ class TypeDeChamp < ApplicationRecord
def self.refresh_after_update?(type_champ) def self.refresh_after_update?(type_champ)
case type_champ case type_champ
when type_champs.fetch(:epci) when type_champs.fetch(:epci), type_champs.fetch(:communes)
true true
else else
false false

View file

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

View file

@ -47,25 +47,6 @@ class APIGeoService
departements.find { _1[:name] == name }&.dig(:code) departements.find { _1[:name] == name }&.dig(:code)
end 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) def epcis(departement_code)
get_from_api_geo("epcis?codeDepartement=#{departement_code}").sort_by { I18n.transliterate(_1[:name]) } get_from_api_geo("epcis?codeDepartement=#{departement_code}").sort_by { I18n.transliterate(_1[:name]) }
end end
@ -78,18 +59,55 @@ class APIGeoService
epcis(departement_code).find { _1[:name] == name }&.dig(:code) epcis(departement_code).find { _1[:name] == name }&.dig(:code)
end end
def communes(departement_code)
get_from_api_geo("communes?codeDepartement=#{departement_code}&type=commune-actuelle,arrondissement-municipal").sort_by { I18n.transliterate([_1[:name], _1[:postal_code]].join(' ')) }
end
def communes_by_postal_code(postal_code)
if postal_code.size > 3
metro_code = postal_code[0..1]
drom_com_code = postal_code[0..2]
if metro_code == '20'
communes('2A') + communes('2B')
elsif metro_code == '97' || metro_code == '98'
departement_name(drom_com_code) ? communes(drom_com_code) : []
else
departement_name(metro_code) ? communes(metro_code) : []
end
.filter { _1[:postal_code] == postal_code }
.sort_by { I18n.transliterate([_1[:name], _1[:postal_code]].join(' ')) }
else
[]
end
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
private private
def get_from_api_geo(scope, additional_keys: {}) def get_from_api_geo(scope)
Rails.cache.fetch("api_geo_#{scope}", expires_in: 1.year) do Rails.cache.fetch("api_geo_#{scope}", expires_in: 1.year) do
response = Typhoeus.get("#{API_GEO_URL}/#{scope}") response = Typhoeus.get("#{API_GEO_URL}/#{scope}")
JSON.parse(response.body) JSON.parse(response.body).map(&:symbolize_keys).flat_map do |result|
.map(&:symbolize_keys) item = {
.map do |result| name: result[:nom].tr("'", ''),
data = { name: result[:nom].tr("'", ''), code: result[:code] } code: result[:code],
additional_keys.each { |key, value| data = data.merge(key => result[value]) } epci_code: result[:codeEpci],
data departement_code: result[:codeDepartement]
}.compact
if result[:codesPostaux].present?
result[:codesPostaux].map { item.merge(postal_code: _1) }
else
[item]
end end
end
end end
end end

View file

@ -1,4 +1,4 @@
= format_text_value(champ.to_s) = champ.name
- if champ.code? - if champ.code?
%p.text-sm %p.text-sm
Code INSEE : Code INSEE :

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -26,7 +26,7 @@ RSpec.describe Types::DossierType, type: :graphql do
end end
end end
describe 'dossier with champs' do describe 'dossier with champs', vcr: { cassette_name: 'graphql_communes' } do
let(:procedure) { create(:procedure, :published, types_de_champ_public: [{ type: :communes }, { type: :address }, { type: :siret }]) } let(:procedure) { create(:procedure, :published, types_de_champ_public: [{ type: :communes }, { type: :address }, { type: :siret }]) }
let(:dossier) { create(:dossier, :accepte, :with_populated_champs, procedure: procedure) } let(:dossier) { create(:dossier, :accepte, :with_populated_champs, procedure: procedure) }
let(:query) { DOSSIER_WITH_CHAMPS_QUERY } let(:query) { DOSSIER_WITH_CHAMPS_QUERY }

View file

@ -1,19 +1,43 @@
describe Champs::CommuneChamp do describe Champs::CommuneChamp do
let(:type_de_champ) { create(:type_de_champ_communes, libelle: 'Ma commune') } let(:memory_store) { ActiveSupport::Cache.lookup_store(:memory_store) }
let(:champ) { Champs::CommuneChamp.new(value: value, external_id: code_insee, departement: departement, code_departement: code_departement, type_de_champ: type_de_champ) }
let(:value) { 'Châteldon (63290)' } before do
allow(Rails).to receive(:cache).and_return(memory_store)
Rails.cache.clear
end
let(:code_insee) { '63102' } let(:code_insee) { '63102' }
let(:departement) { '' } let(:code_postal) { '63290' }
let(:code_departement) { '' } let(:code_departement) { '63' }
it { expect(champ.value).to eq('Châteldon (63290)') } describe 'value', vcr: { cassette_name: 'api_geo_communes' } do
it { expect(champ.external_id).to eq('63102') } let(:champ) { create(:champ_communes, code_postal: code_postal) }
it { expect(champ.for_export).to eq(['Châteldon (63290)', '63102', '']) }
context do it 'with code_postal' do
let(:departement) { 'Puy-de-Dôme' } champ.update(value: code_insee)
let(:code_departement) { '63' } expect(champ.name).to eq('Châteldon (63290)')
expect(champ.external_id).to eq(code_insee)
expect(champ.code).to eq(code_insee)
expect(champ.code_departement).to eq(code_departement)
expect(champ.code_postal).to eq(code_postal)
expect(champ.for_export).to eq(['Châteldon (63290)', '63102', '63 Puy-de-Dôme'])
expect(champ.communes.size).to eq(8)
end
it { expect(champ.for_export).to eq(['Châteldon (63290)', '63102', '63 - Puy-de-Dôme']) } context 'when code_postal is nil', vcr: { cassette_name: 'api_geo_communes' } do
let(:champ) { create(:champ_communes, external_id: code_insee, code_departement:) }
it 'with value' do
champ.update_column(:value, 'Châteldon (63290)')
expect(champ.name).to eq('Châteldon (63290)')
expect(champ.external_id).to eq(code_insee)
expect(champ.code).to eq(code_insee)
expect(champ.code_departement).to eq(code_departement)
expect(champ.code_postal).to be_nil
expect(champ.code_postal_with_fallback).to eq(code_postal)
expect(champ.for_export).to eq(['Châteldon (63290)', '63102', '63 Puy-de-Dôme'])
expect(champ.communes.size).to eq(8)
end
end
end end
end end

View file

@ -1573,7 +1573,7 @@ describe Dossier do
end end
end end
describe "champs_for_export" do describe "champs_for_export", vcr: { cassette_name: 'api_geo_communes' } do
context 'with a unconditionnal procedure' do context 'with a unconditionnal procedure' do
let(:procedure) { create(:procedure, :with_type_de_champ, :with_datetime, :with_yes_no, :with_explication, :with_commune, :with_repetition, zones: [create(:zone)]) } let(:procedure) { create(:procedure, :with_type_de_champ, :with_datetime, :with_yes_no, :with_explication, :with_commune, :with_repetition, zones: [create(:zone)]) }
let(:text_type_de_champ) { procedure.active_revision.types_de_champ_public.find { |type_de_champ| type_de_champ.type_champ == TypeDeChamp.type_champs.fetch(:text) } } let(:text_type_de_champ) { procedure.active_revision.types_de_champ_public.find { |type_de_champ| type_de_champ.type_champ == TypeDeChamp.type_champs.fetch(:text) } }

View file

@ -139,7 +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", :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", :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", :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", :communes, ['01540', '01457']
it_behaves_like "a champ public value that is authorized", :address, "20 avenue de Ségur 75007 Paris" it_behaves_like "a champ public value that is authorized", :address, "20 avenue de Ségur 75007 Paris"
it_behaves_like "a champ public value that is authorized", :annuaire_education, "0050009H" it_behaves_like "a champ public value that is authorized", :annuaire_education, "0050009H"
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", :multiple_drop_down_list, ["val1", "val2"]
@ -183,7 +183,7 @@ RSpec.describe PrefillParams do
it_behaves_like "a champ private value that is authorized", :rna, "value" it_behaves_like "a champ private value that is authorized", :rna, "value"
it_behaves_like "a champ private value that is authorized", :siret, "13002526500013" it_behaves_like "a champ private value that is authorized", :siret, "13002526500013"
it_behaves_like "a champ private value that is authorized", :departements, "03" 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", :communes, ['01540', '01457']
it_behaves_like "a champ private value that is authorized", :address, "20 avenue de Ségur 75007 Paris" it_behaves_like "a champ private value that is authorized", :address, "20 avenue de Ségur 75007 Paris"
it_behaves_like "a champ private value that is authorized", :annuaire_education, "0050009H" it_behaves_like "a champ private value that is authorized", :annuaire_education, "0050009H"
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", :multiple_drop_down_list, ["val1", "val2"]

View file

@ -26,21 +26,21 @@ RSpec.describe TypesDeChamp::PrefillCommuneTypeDeChamp do
it { is_expected.to be_kind_of(TypesDeChamp::PrefillTypeDeChamp) } it { is_expected.to be_kind_of(TypesDeChamp::PrefillTypeDeChamp) }
end end
describe '#all_possible_values' do # describe '#all_possible_values' do
let(:expected_values) do # let(:expected_values) do
departements.map { |departement| "#{departement[:code]} (#{departement[:name]}) : https://geo.api.gouv.fr/communes?codeDepartement=#{departement[:code]}" } # departements.map { |departement| "#{departement[:code]} (#{departement[:name]}) : https://geo.api.gouv.fr/communes?codeDepartement=#{departement[:code]}" }
end # end
subject(:all_possible_values) { described_class.new(type_de_champ, procedure.active_revision).all_possible_values } # subject(:all_possible_values) { described_class.new(type_de_champ, procedure.active_revision).all_possible_values }
it { expect(all_possible_values).to match(expected_values) } # it { expect(all_possible_values).to match(expected_values) }
end # end
describe '#example_value' do describe '#example_value' do
let(:departement_code) { departements.pick(:code) } let(:departement_code) { departements.pick(:code) }
let(:commune_code) { APIGeoService.communes(departement_code).pick(:code) } let(:value) { APIGeoService.communes(departement_code).pick(:postal_code, :code) }
subject(:example_value) { described_class.new(type_de_champ, procedure.active_revision).example_value } subject(:example_value) { described_class.new(type_de_champ, procedure.active_revision).example_value }
it { is_expected.to eq([departement_code, commune_code]) } it { is_expected.to eq(value) }
end end
describe '#to_assignable_attributes' do describe '#to_assignable_attributes' do
@ -65,22 +65,22 @@ RSpec.describe TypesDeChamp::PrefillCommuneTypeDeChamp do
end end
context 'when the value is an array of one element' do context 'when the value is an array of one element' do
context 'when the first element is a valid departement code' do context 'when the first element is a valid postal code' do
let(:value) { ['01'] } let(:value) { ['01540'] }
it { is_expected.to match({ id: champ.id, code_departement: '01', departement: 'Ain' }) } it { is_expected.to match({ id: champ.id, code_postal: '01540' }) }
end end
context 'when the first element is not a valid departement code' do context 'when the first element is not a valid postal code' do
let(:value) { ['totoro'] } let(:value) { ['totoro'] }
it { is_expected.to match(nil) } it { is_expected.to match(nil) }
end end
end end
context 'when the value is an array of two elements' do 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 first element is a valid postal code' do
context 'when the second element is a valid insee code' do context 'when the second element is a valid insee code' do
let(:value) { ['01', '01457'] } let(:value) { ['01540', '01457'] }
it { is_expected.to match({ id: champ.id, code_departement: '01', departement: 'Ain', external_id: '01457', value: 'Vonnas (01540)' }) } it { is_expected.to match({ id: champ.id, code_postal: '01540', value: '01457' }) }
end end
context 'when the second element is not a valid insee code' do context 'when the second element is not a valid insee code' do
@ -89,26 +89,26 @@ RSpec.describe TypesDeChamp::PrefillCommuneTypeDeChamp do
end end
end end
context 'when the first element is not a valid departement code' do context 'when the first element is not a valid postal code' do
let(:value) { ['totoro', '01457'] } let(:value) { ['totoro', '01457'] }
it { is_expected.to match(nil) } it { is_expected.to match(nil) }
end end
end end
context 'when the value is an array of three or more elements' do 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 first element is a valid postal code' do
context 'when the second element is a valid insee code' do context 'when the second element is a valid insee code' do
let(:value) { ['01', '01457', 'hello'] } let(:value) { ['01540', '01457', 'hello'] }
it { is_expected.to match({ id: champ.id, code_departement: '01', departement: 'Ain', external_id: '01457', value: 'Vonnas (01540)' }) } it { is_expected.to match({ id: champ.id, code_postal: '01540', value: '01457' }) }
end end
context 'when the second element is not a valid insee code' do context 'when the second element is not a valid insee code' do
let(:value) { ['01', 'totoro', 'hello'] } let(:value) { ['01540', 'totoro', 'hello'] }
it { is_expected.to match(nil) } it { is_expected.to match(nil) }
end end
end end
context 'when the first element is not a valid departement code' do context 'when the first element is not a valid postal code' do
let(:value) { ['totoro', '01457', 'hello'] } let(:value) { ['totoro', '01457', 'hello'] }
it { is_expected.to match(nil) } it { is_expected.to match(nil) }
end end

View file

@ -45,9 +45,19 @@ describe APIGeoService do
describe 'communes', vcr: { cassette_name: 'api_geo_communes' } do describe 'communes', vcr: { cassette_name: 'api_geo_communes' } do
it 'return sorted results' do it 'return sorted results' do
expect(APIGeoService.communes('01').size).to eq(393) expect(APIGeoService.communes('01').size).to eq(399)
expect(APIGeoService.communes('01').first).to eq(code: '01004', name: 'Ambérieu-en-Bugey', postal_codes: ['01500']) expect(APIGeoService.communes('01').first).to eq(code: '01004', name: 'Ambérieu-en-Bugey', postal_code: '01500', departement_code: '01', epci_code: '240100883')
expect(APIGeoService.communes('01').last).to eq(code: '01457', name: 'Vonnas', postal_codes: ['01540']) expect(APIGeoService.communes('01').last).to eq(code: '01457', name: 'Vonnas', postal_code: '01540', departement_code: '01', epci_code: '200070555')
end
end
describe 'communes_by_postal_code', vcr: { cassette_name: 'api_geo_communes' } do
it 'return results' do
expect(APIGeoService.communes_by_postal_code('75019').size).to eq(2)
expect(APIGeoService.communes_by_postal_code('69005').size).to eq(2)
expect(APIGeoService.communes_by_postal_code('13006').size).to eq(2)
expect(APIGeoService.communes_by_postal_code('73480').size).to eq(3)
expect(APIGeoService.communes_by_postal_code('20000').first[:code]).to eq('2A004')
end end
end end
@ -61,11 +71,6 @@ describe APIGeoService do
it { is_expected.to eq('01457') } it { is_expected.to eq('01457') }
end 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 describe 'epcis', vcr: { cassette_name: 'api_geo_epcis' } do
it 'return sorted results' do it 'return sorted results' do
expect(APIGeoService.epcis('01').size).to eq(17) expect(APIGeoService.epcis('01').size).to eq(17)

View file

@ -25,7 +25,7 @@ shared_examples "the user has got a prefilled dossier, owned by themselves" do
expect(page).to have_content(multiple_drop_down_list_values.last) 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_field(type_de_champ_epci.libelle, with: epci_value.last)
expect(page).to have_field(type_de_champ_dossier_link.libelle, with: dossier_link_value) expect(page).to have_field(type_de_champ_dossier_link.libelle, with: dossier_link_value)
expect(page).to have_selector("input[value='Vonnas (01540)']") expect(page).to have_field(commune_libelle, with: '01457')
expect(page).to have_content(annuaire_education_value.last) expect(page).to have_content(annuaire_education_value.last)
expect(page).to have_content(address_value.last) expect(page).to have_content(address_value.last)
end end

View file

@ -44,8 +44,8 @@ describe 'The user' do
select('Australie', from: form_id_for('pays')) select('Australie', from: form_id_for('pays'))
select('Martinique', from: form_id_for('regions')) select('Martinique', from: form_id_for('regions'))
select('02 Aisne', from: form_id_for('departements')) select('02 Aisne', from: form_id_for('departements'))
select_combobox('communes', 'Ai', '02 - Aisne', check: false) fill_in('Code postal de la commune', with: '60400')
select_combobox('communes', 'Ambl', 'Ambléon (01300)') select('Brétigny (60400)', from: form_id_for('communes'))
fill_in('dossier_link', with: '123') fill_in('dossier_link', with: '123')
find('.editable-champ-piece_justificative input[type=file]').attach_file(Rails.root + 'spec/fixtures/files/file.pdf') find('.editable-champ-piece_justificative input[type=file]').attach_file(Rails.root + 'spec/fixtures/files/file.pdf')
@ -74,7 +74,7 @@ describe 'The user' do
expect(champ_value_for('pays')).to eq('Australie') expect(champ_value_for('pays')).to eq('Australie')
expect(champ_value_for('regions')).to eq('Martinique') expect(champ_value_for('regions')).to eq('Martinique')
expect(champ_value_for('departements')).to eq('Aisne') expect(champ_value_for('departements')).to eq('Aisne')
expect(champ_value_for('communes')).to eq('Ambléon (01300)') expect(champ_value_for('communes')).to eq('Brétigny')
expect(champ_value_for('dossier_link')).to eq('123') expect(champ_value_for('dossier_link')).to eq('123')
expect(champ_value_for('piece_justificative')).to be_nil # antivirus hasn't approved the file yet expect(champ_value_for('piece_justificative')).to be_nil # antivirus hasn't approved the file yet
@ -98,7 +98,8 @@ describe 'The user' do
expect(page).to have_selected_value('regions', selected: 'Martinique') expect(page).to have_selected_value('regions', selected: 'Martinique')
expect(page).to have_selected_value('departements', selected: '02 Aisne') expect(page).to have_selected_value('departements', selected: '02 Aisne')
check_selected_value('multiple_choice_drop_down_list_long', with: ['alpha', 'charly']) check_selected_value('multiple_choice_drop_down_list_long', with: ['alpha', 'charly'])
check_selected_value('communes', with: 'Ambléon (01300)') expect(page).to have_selected_value('communes', selected: 'Brétigny (60400)')
expect(page).to have_selected_value('pays', selected: 'Australie')
expect(page).to have_field('dossier_link', with: '123') expect(page).to have_field('dossier_link', with: '123')
expect(page).to have_text('file.pdf') expect(page).to have_text('file.pdf')
expect(page).to have_text('Analyse antivirus en cours') expect(page).to have_text('Analyse antivirus en cours')

View file

@ -32,7 +32,8 @@ describe 'Prefilling a dossier (with a GET request):', js: true do
} }
let(:epci_value) { ['01', '200029999'] } let(:epci_value) { ['01', '200029999'] }
let(:dossier_link_value) { '42' } let(:dossier_link_value) { '42' }
let(:commune_value) { ['01', '01457'] } # Vonnas (01540) let(:commune_value) { ['01540', '01457'] } # Vonnas (01540)
let(:commune_libelle) { 'Vonnas (01540)' }
let(:address_value) { "20 Avenue de Ségur 75007 Paris" } let(:address_value) { "20 Avenue de Ségur 75007 Paris" }
let(:sub_type_de_champs_repetition) { procedure.active_revision.children_of(type_de_champ_repetition) } 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(:text_repetition_libelle) { sub_type_de_champs_repetition.first.libelle }

View file

@ -31,7 +31,8 @@ describe 'Prefilling a dossier (with a POST request):', js: true do
] ]
} }
let(:epci_value) { ['01', '200029999'] } let(:epci_value) { ['01', '200029999'] }
let(:commune_value) { ['01', '01457'] } # Vonnas (01540) let(:commune_value) { ['01540', '01457'] } # Vonnas (01540)
let(:commune_libelle) { 'Vonnas (01540)' }
let(:address_value) { "20 Avenue de Ségur 75007 Paris" } let(:address_value) { "20 Avenue de Ségur 75007 Paris" }
let(:sub_type_de_champs_repetition) { procedure.active_revision.children_of(type_de_champ_repetition) } 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(:text_repetition_libelle) { sub_type_de_champs_repetition.first.libelle }