Merge pull request #8454 from tchak/feat-epci

feat(types de champ): add EPCI champ
This commit is contained in:
mfo 2023-01-23 16:56:18 +01:00 committed by GitHub
commit 84a667b8bd
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
28 changed files with 376 additions and 21 deletions

View file

@ -0,0 +1,29 @@
class EditableChamp::EpciComponent < EditableChamp::EditableChampBaseComponent
include ApplicationHelper
private
def departement_options
APIGeoService.departements.filter { _1[:code] != '99' }.map { ["#{_1[:code]} #{_1[:name]}", _1[:code]] }
end
def epci_options
if @champ.departement?
APIGeoService.epcis(@champ.code_departement).map { ["#{_1[:code]} #{_1[:name]}", _1[:code]] }
else
[]
end
end
def departement_input_id
"#{@champ.input_id}-departement"
end
def departement_select_options
{ selected: @champ.code_departement }.merge(@champ.mandatory? ? { prompt: '' } : { include_blank: '' })
end
def epci_select_options
{ selected: @champ.code }.merge(@champ.mandatory? ? { prompt: '' } : { include_blank: '' })
end
end

View file

@ -0,0 +1,4 @@
%label.notice{ for: departement_input_id } Le département de lEPCI
= @form.select :code_departement, departement_options, departement_select_options, required: @champ.mandatory?, id: departement_input_id, class: "width-33-desktop width-100-mobile"
- if @champ.departement?
= @form.select :value, epci_options, epci_select_options, required: @champ.mandatory?, id: @champ.input_id, aria: { describedby: @champ.describedby_id }, class: "width-33-desktop width-100-mobile"

View file

@ -60,6 +60,10 @@ class TypesDeChampEditor::ChampComponent < ApplicationComponent
TypeDeChamp.type_champs
.keys
# FIXME
# We can only refresh after update champs when autosave is enabled. And it is disabled for now in private forms.
# So for new we restrict champs that require refresh after update to public forms.
.filter { type_de_champ.public? || !TypeDeChamp.refresh_after_update?(_1) }
.filter(&method(:filter_type_champ))
.filter(&method(:filter_featured_type_champ))
.filter(&method(:filter_block_type_champ))

View file

@ -201,10 +201,7 @@ module Users
respond_to do |format|
format.html { render :brouillon }
format.turbo_stream do
@to_shows, @to_hides = @dossier.champs_public_all
.filter(&:conditional?)
.partition(&:visible?)
.map { |champs| champs_to_one_selector(champs) }
@to_show, @to_hide, @to_update = champs_to_turbo_update
render(:update, layout: false)
end
@ -222,10 +219,7 @@ module Users
respond_to do |format|
format.html { render :modifier }
format.turbo_stream do
@to_shows, @to_hides = @dossier.champs_public_all
.filter(&:conditional?)
.partition(&:visible?)
.map { |champs| champs_to_one_selector(champs) }
@to_show, @to_hide, @to_update = champs_to_turbo_update
end
end
end
@ -493,6 +487,24 @@ module Users
errors
end
def champs_to_turbo_update
champ_ids = champs_public_params
.fetch(:champs_public_all_attributes)
.keys
.map(&:to_i)
to_update = dossier
.champs_public_all
.filter { _1.id.in?(champ_ids) && _1.refresh_after_update? }
to_show, to_hide = dossier
.champs_public_all
.filter(&:conditional?)
.partition(&:visible?)
.map { champs_to_one_selector(_1 - to_update) }
return to_show, to_hide, to_update
end
def ensure_ownership!
if !current_user.owns?(dossier)
forbidden!

View file

@ -62,6 +62,7 @@ class API::V2::Schema < GraphQL::Schema
Types::Champs::DecimalNumberChampType,
Types::Champs::DepartementChampType,
Types::Champs::DossierLinkChampType,
Types::Champs::EpciChampType,
Types::Champs::IntegerNumberChampType,
Types::Champs::LinkedDropDownListChampType,
Types::Champs::MultipleDropDownListChampType,
@ -91,6 +92,7 @@ class API::V2::Schema < GraphQL::Schema
Types::Champs::Descriptor::DossierLinkChampDescriptorType,
Types::Champs::Descriptor::DropDownListChampDescriptorType,
Types::Champs::Descriptor::EmailChampDescriptorType,
Types::Champs::Descriptor::EpciChampDescriptorType,
Types::Champs::Descriptor::ExplicationChampDescriptorType,
Types::Champs::Descriptor::HeaderSectionChampDescriptorType,
Types::Champs::Descriptor::IbanChampDescriptorType,
@ -107,8 +109,8 @@ class API::V2::Schema < GraphQL::Schema
Types::Champs::Descriptor::RepetitionChampDescriptorType,
Types::Champs::Descriptor::RNAChampDescriptorType,
Types::Champs::Descriptor::SiretChampDescriptorType,
Types::Champs::Descriptor::TextChampDescriptorType,
Types::Champs::Descriptor::TextareaChampDescriptorType,
Types::Champs::Descriptor::TextChampDescriptorType,
Types::Champs::Descriptor::TitreIdentiteChampDescriptorType,
Types::Champs::Descriptor::YesNoChampDescriptorType

View file

@ -1978,6 +1978,55 @@ enum EntrepriseEtatAdministratif {
Ferme
}
type Epci {
code: String!
name: String!
}
type EpciChamp implements Champ {
departement: Departement
epci: Epci
id: ID!
"""
Libellé du champ.
"""
label: String!
"""
La valeur du champ sous forme texte.
"""
stringValue: String
}
type EpciChampDescriptor implements ChampDescriptor {
"""
Description des champs dun bloc répétable.
"""
champDescriptors: [ChampDescriptor!] @deprecated(reason: "Utilisez le champ `RepetitionChampDescriptor.champ_descriptors` à la place.")
"""
Description du champ.
"""
description: String
id: ID!
"""
Libellé du champ.
"""
label: String!
"""
Est-ce que le champ est obligatoire ?
"""
required: Boolean!
"""
Type de la valeur du champ.
"""
type: TypeDeChamp! @deprecated(reason: "Utilisez le champ `__typename` à la place.")
}
type ExplicationChampDescriptor implements ChampDescriptor {
"""
Description des champs dun bloc répétable.
@ -3542,6 +3591,11 @@ enum TypeDeChamp {
"""
email
"""
EPCI
"""
epci
"""
Explication
"""

View file

@ -92,6 +92,8 @@ module Types
Types::Champs::Descriptor::PoleEmploiChampDescriptorType
when TypeDeChamp.type_champs.fetch(:mesri)
Types::Champs::Descriptor::MesriChampDescriptorType
when TypeDeChamp.type_champs.fetch(:epci)
Types::Champs::Descriptor::EpciChampDescriptorType
end
end
end

View file

@ -71,6 +71,8 @@ module Types
Types::Champs::CiviliteChampType
when ::Champs::TitreIdentiteChamp
Types::Champs::TitreIdentiteChampType
when ::Champs::EpciChamp
Types::Champs::EpciChampType
else
Types::Champs::TextChampType
end

View file

@ -0,0 +1,5 @@
module Types::Champs::Descriptor
class EpciChampDescriptorType < Types::BaseObject
implements Types::ChampDescriptorType
end
end

View file

@ -0,0 +1,23 @@
module Types::Champs
class EpciChampType < Types::BaseObject
implements Types::ChampType
class EpciType < Types::BaseObject
field :name, String, null: false
field :code, String, null: false
end
field :epci, EpciType, null: true
field :departement, Types::Champs::DepartementChampType::DepartementType, null: true
def epci
object if object.external_id.present?
end
def departement
if object.departement?
object.departement
end
end
end
end

View file

@ -67,6 +67,7 @@ class Champ < ApplicationRecord
:stable_id,
:mandatory?,
:prefillable?,
:refresh_after_update?,
to: :type_de_champ
scope :updated_since?, -> (date) { where('champs.updated_at > ?', date) }

View file

@ -0,0 +1,77 @@
# == Schema Information
#
# Table name: champs
#
# id :integer not null, primary key
# data :jsonb
# fetch_external_data_exceptions :string is an Array
# prefilled :boolean default(FALSE)
# private :boolean default(FALSE), not null
# rebased_at :datetime
# type :string
# value :string
# value_json :jsonb
# created_at :datetime
# updated_at :datetime
# dossier_id :integer
# etablissement_id :integer
# external_id :string
# parent_id :bigint
# row_id :string
# type_de_champ_id :integer
#
class Champs::EpciChamp < Champs::TextChamp
store_accessor :value_json, :code_departement
before_validation :on_departement_change
def for_export
[value, code, "#{code_departement} #{departement_name}"]
end
def departement_name
APIGeoService.departement_name(code_departement)
end
def departement
{ code: code_departement, name: departement_name }
end
def departement?
code_departement.present?
end
def code?
code.present?
end
def name
value
end
def code
external_id
end
def selected
code
end
def value=(code)
if code.blank? || !departement?
self.external_id = nil
super(nil)
else
self.external_id = code
super(APIGeoService.epci_name(code_departement, code))
end
end
private
def on_departement_change
if code_departement_changed?
self.external_id = nil
self.value = nil
end
end
end

View file

@ -44,6 +44,7 @@ class TypeDeChamp < ApplicationRecord
departements: LOCALISATION,
regions: LOCALISATION,
pays: LOCALISATION,
epci: LOCALISATION,
iban: PAIEMENT_IDENTIFICATION,
siret: PAIEMENT_IDENTIFICATION,
text: STANDARD,
@ -104,7 +105,8 @@ class TypeDeChamp < ApplicationRecord
cnaf: 'cnaf',
dgfip: 'dgfip',
pole_emploi: 'pole_emploi',
mesri: 'mesri'
mesri: 'mesri',
epci: 'epci'
}
store_accessor :options,
@ -469,6 +471,19 @@ class TypeDeChamp < ApplicationRecord
model_name: OpenStruct.new(param_key: model_name.param_key))
end
def refresh_after_update?
self.class.refresh_after_update?(type_champ)
end
def self.refresh_after_update?(type_champ)
case type_champ
when type_champs.fetch(:epci)
true
else
false
end
end
private
DEFAULT_EMPTY = ['']

View file

@ -0,0 +1,5 @@
class TypesDeChamp::EpciTypeDeChamp < TypesDeChamp::TextTypeDeChamp
def libelle_for_export(index)
[libelle, "#{libelle} (Code)", "#{libelle} (Département)"][index]
end
end

View file

@ -47,6 +47,18 @@ class APIGeoService
departements.find { _1[:name] == name }&.dig(:code)
end
def epcis(departement_code)
get_from_api_geo("epcis?codeDepartement=#{departement_code}").sort_by { I18n.transliterate(_1[:name]) }
end
def epci_name(departement_code, code)
epcis(departement_code).find { _1[:code] == code }&.dig(:name)
end
def epci_code(departement_code, name)
epcis(departement_code).find { _1[:name] == name }&.dig(:code)
end
private
def get_from_api_geo(scope)

View file

@ -0,0 +1,4 @@
- if champ.code?
= format_text_value("#{champ.code} #{champ}")
- else
= format_text_value(champ.to_s)

View file

@ -52,6 +52,8 @@
= render partial: "shared/champs/regions/show", locals: { champ: c }
- when TypeDeChamp.type_champs.fetch(:rna)
= render partial: "shared/champs/rna/show", locals: { champ: c, profile: profile }
- when TypeDeChamp.type_champs.fetch(:epci)
= render partial: "shared/champs/epci/show", locals: { champ: c }
- when TypeDeChamp.type_champs.fetch(:date)
= c.to_s
- when TypeDeChamp.type_champs.fetch(:datetime)

View file

@ -1,4 +1,8 @@
- if @to_shows.present?
= turbo_stream.show_all(@to_shows)
- if @to_hides.present?
= turbo_stream.hide_all(@to_hides)
- if @to_show.present?
= turbo_stream.show_all(@to_show)
- if @to_hide.present?
= turbo_stream.hide_all(@to_hide)
- @to_update.each do |champ|
= fields_for champ.input_name, champ do |form|
= turbo_stream.morph champ.input_group_id do
= render EditableChamp::EditableChampComponent.new champ:, form:

View file

@ -49,3 +49,4 @@ en:
dgfip: 'Data from Direction générale des Finances publiques'
pole_emploi: 'Pôle emploi status'
mesri: "Data from Ministère de lEnseignement Supérieur, de la Recherche et de lInnovation"
epci: "EPCI"

View file

@ -49,3 +49,4 @@ fr:
dgfip: 'Données de la Direction générale des Finances publiques'
pole_emploi: 'Situation Pôle emploi'
mesri: "Données du Ministère de lEnseignement Supérieur, de la Recherche et de lInnovation"
epci: "EPCI"

View file

@ -19,11 +19,14 @@ describe Administrateurs::ProceduresController, type: :controller do
render_views
let(:procedure) { create(:procedure, :with_all_champs) }
let(:memory_store) { ActiveSupport::Cache.lookup_store(:memory_store) }
subject { get :apercu, params: { id: procedure.id } }
before do
sign_in(admin.user)
allow(Rails).to receive(:cache).and_return(memory_store)
Rails.cache.clear
end
it do

View file

@ -125,6 +125,12 @@ FactoryBot.define do
value { 'Paris' }
end
factory :champ_epci, class: 'Champs::EpciChamp' do
type_de_champ { association :type_de_champ_epci, procedure: dossier.procedure }
value { 'CC Retz en Valois' }
external_id { '200071991' }
end
factory :champ_header_section, class: 'Champs::HeaderSectionChamp' do
type_de_champ { association :type_de_champ_header_section, procedure: dossier.procedure }
value { 'une section' }

View file

@ -164,6 +164,9 @@ FactoryBot.define do
factory :type_de_champ_carte do
type_champ { TypeDeChamp.type_champs.fetch(:carte) }
end
factory :type_de_champ_epci do
type_champ { TypeDeChamp.type_champs.fetch(:epci) }
end
factory :type_de_champ_repetition do
type_champ { TypeDeChamp.type_champs.fetch(:repetition) }

View file

@ -0,0 +1,41 @@
---
http_interactions:
- request:
method: get
uri: https://geo.api.gouv.fr/epcis?codeDepartement=01
body:
encoding: US-ASCII
string: ''
headers:
User-Agent:
- demarches-simplifiees.fr
Expect:
- ''
response:
status:
code: 200
message: ''
headers:
Server:
- nginx/1.10.3 (Ubuntu)
Date:
- Fri, 20 Jan 2023 10:11:24 GMT
Content-Type:
- application/json; charset=utf-8
Content-Length:
- '2108'
Vary:
- Accept-Encoding
- Origin
X-Powered-By:
- Express
Etag:
- W/"83c-Ql239C1pTEYucFRGeoG5NpJkDYY"
Strict-Transport-Security:
- max-age=15552000
body:
encoding: ASCII-8BIT
string: !binary |-
W3sibm9tIjoiQ0MgUml2ZXMgZGUgbCdBaW4gLSBQYXlzIGR1IENlcmRvbiIsImNvZGUiOiIyMDAwMjk5OTkiLCJjb2Rlc0RlcGFydGVtZW50cyI6WyIwMSJdLCJjb2Rlc1JlZ2lvbnMiOlsiODQiXSwicG9wdWxhdGlvbiI6MTQ2NzF9LHsibm9tIjoiQ0MgQnVnZXkgU3VkIiwiY29kZSI6IjIwMDA0MDM1MCIsImNvZGVzRGVwYXJ0ZW1lbnRzIjpbIjAxIl0sImNvZGVzUmVnaW9ucyI6WyI4NCJdLCJwb3B1bGF0aW9uIjozNDA1N30seyJub20iOiJDQSBWaWxsZWZyYW5jaGUgQmVhdWpvbGFpcyBTYcO0bmUiLCJjb2RlIjoiMjAwMDQwNTkwIiwiY29kZXNEZXBhcnRlbWVudHMiOlsiMDEiLCI2OSJdLCJjb2Rlc1JlZ2lvbnMiOlsiODQiXSwicG9wdWxhdGlvbiI6NzI4MTV9LHsibm9tIjoiQ0MgRG9tYmVzIFNhw7RuZSBWYWxsw6llIiwiY29kZSI6IjIwMDA0MjQ5NyIsImNvZGVzRGVwYXJ0ZW1lbnRzIjpbIjAxIl0sImNvZGVzUmVnaW9ucyI6WyI4NCJdLCJwb3B1bGF0aW9uIjozOTExOX0seyJub20iOiJDQSBIYXV0IC0gQnVnZXkgQWdnbG9tw6lyYXRpb24iLCJjb2RlIjoiMjAwMDQyOTM1IiwiY29kZXNEZXBhcnRlbWVudHMiOlsiMDEiXSwiY29kZXNSZWdpb25zIjpbIjg0Il0sInBvcHVsYXRpb24iOjYzMzY1fSx7Im5vbSI6IkNDIGRlIGxhIERvbWJlcyIsImNvZGUiOiIyMDAwNjkxOTMiLCJjb2Rlc0RlcGFydGVtZW50cyI6WyIwMSJdLCJjb2Rlc1JlZ2lvbnMiOlsiODQiXSwicG9wdWxhdGlvbiI6MzkzODN9LHsibm9tIjoiQ0MgVmFsIGRlIFNhw7RuZSBDZW50cmUiLCJjb2RlIjoiMjAwMDcwMTE4IiwiY29kZXNEZXBhcnRlbWVudHMiOlsiMDEiXSwiY29kZXNSZWdpb25zIjpbIjg0Il0sInBvcHVsYXRpb24iOjIwNjUxfSx7Im5vbSI6IkNBIE3DomNvbm5haXMgQmVhdWpvbGFpcyBBZ2dsb23DqXJhdGlvbiIsImNvZGUiOiIyMDAwNzAzMDgiLCJjb2Rlc0RlcGFydGVtZW50cyI6WyIwMSIsIjcxIl0sImNvZGVzUmVnaW9ucyI6WyIyNyIsIjg0Il0sInBvcHVsYXRpb24iOjc4MjgxfSx7Im5vbSI6IkNDIGRlIGxhIFZleWxlIiwiY29kZSI6IjIwMDA3MDU1NSIsImNvZGVzRGVwYXJ0ZW1lbnRzIjpbIjAxIl0sImNvZGVzUmVnaW9ucyI6WyI4NCJdLCJwb3B1bGF0aW9uIjoyMjk0MH0seyJub20iOiJDQyBVc3NlcyBldCBSaMO0bmUiLCJjb2RlIjoiMjAwMDcwODUyIiwiY29kZXNEZXBhcnRlbWVudHMiOlsiMDEiLCI3NCJdLCJjb2Rlc1JlZ2lvbnMiOlsiODQiXSwicG9wdWxhdGlvbiI6MjA4MzZ9LHsibm9tIjoiQ0MgQnJlc3NlIGV0IFNhw7RuZSIsImNvZGUiOiIyMDAwNzEzNzEiLCJjb2Rlc0RlcGFydGVtZW50cyI6WyIwMSJdLCJjb2Rlc1JlZ2lvbnMiOlsiODQiXSwicG9wdWxhdGlvbiI6MjUzODh9LHsibm9tIjoiQ0EgZHUgQmFzc2luIGRlIEJvdXJnLWVuLUJyZXNzZSIsImNvZGUiOiIyMDAwNzE3NTEiLCJjb2Rlc0RlcGFydGVtZW50cyI6WyIwMSJdLCJjb2Rlc1JlZ2lvbnMiOlsiODQiXSwicG9wdWxhdGlvbiI6MTMzMTIwfSx7Im5vbSI6IkNDIGRlIGxhIEPDtHRpw6hyZSDDoCBNb250bHVlbCIsImNvZGUiOiIyNDAxMDA2MTAiLCJjb2Rlc0RlcGFydGVtZW50cyI6WyIwMSJdLCJjb2Rlc1JlZ2lvbnMiOlsiODQiXSwicG9wdWxhdGlvbiI6MjQ4NjR9LHsibm9tIjoiQ0EgZHUgUGF5cyBkZSBHZXgiLCJjb2RlIjoiMjQwMTAwNzUwIiwiY29kZXNEZXBhcnRlbWVudHMiOlsiMDEiXSwiY29kZXNSZWdpb25zIjpbIjg0Il0sInBvcHVsYXRpb24iOjk4MjU3fSx7Im5vbSI6IkNDIGRlIE1pcmliZWwgZXQgZHUgUGxhdGVhdSIsImNvZGUiOiIyNDAxMDA4MDAiLCJjb2Rlc0RlcGFydGVtZW50cyI6WyIwMSJdLCJjb2Rlc1JlZ2lvbnMiOlsiODQiXSwicG9wdWxhdGlvbiI6MjQyNzB9LHsibm9tIjoiQ0MgZGUgbGEgUGxhaW5lIGRlIGwnQWluIiwiY29kZSI6IjI0MDEwMDg4MyIsImNvZGVzRGVwYXJ0ZW1lbnRzIjpbIjAxIl0sImNvZGVzUmVnaW9ucyI6WyI4NCJdLCJwb3B1bGF0aW9uIjo3OTA2M30seyJub20iOiJDQyBkdSBQYXlzIEJlbGxlZ2FyZGllbiAoQ0NQQikiLCJjb2RlIjoiMjQwMTAwODkxIiwiY29kZXNEZXBhcnRlbWVudHMiOlsiMDEiXSwiY29kZXNSZWdpb25zIjpbIjg0Il0sInBvcHVsYXRpb24iOjIxODY1fV0=
recorded_at: Fri, 20 Jan 2023 10:11:24 GMT
recorded_with: VCR 6.1.0

View file

@ -17,9 +17,9 @@ describe '20220705164551_remove_unused_champs' do
describe 'remove_unused_champs' do
it "with bad champs" do
expect(Champ.where(dossier: dossier).count).to eq(37)
expect(Champ.where(dossier: dossier).count).to eq(38)
run_task
expect(Champ.where(dossier: dossier).count).to eq(36)
expect(Champ.where(dossier: dossier).count).to eq(37)
end
end
end

View file

@ -0,0 +1,23 @@
describe Champs::EpciChamp, type: :model do
let(:memory_store) { ActiveSupport::Cache.lookup_store(:memory_store) }
before do
allow(Rails).to receive(:cache).and_return(memory_store)
Rails.cache.clear
end
let(:champ) { described_class.new }
describe 'value', vcr: { cassette_name: 'api_geo_epcis' } do
it 'with departement and code' do
champ.code_departement = '01'
champ.value = '200042935'
expect(champ.external_id).to eq('200042935')
expect(champ.value).to eq('CA Haut - Bugey Agglomération')
expect(champ.selected).to eq('200042935')
expect(champ.code).to eq('200042935')
expect(champ.departement?).to be_truthy
expect(champ.to_s).to eq('CA Haut - Bugey Agglomération')
end
end
end

View file

@ -42,4 +42,11 @@ describe APIGeoService do
expect(APIGeoService.departements.last).to eq(code: '976', name: 'Mayotte')
end
end
describe 'epcis', vcr: { cassette_name: 'api_geo_epcis' } do
it 'return sorted results' do
expect(APIGeoService.epcis('01').size).to eq(17)
expect(APIGeoService.epcis('01').first).to eq(code: '200042935', name: 'CA Haut - Bugey Agglomération')
end
end
end

View file

@ -1,10 +1,14 @@
require 'csv'
describe ProcedureExportService do
describe ProcedureExportService, vcr: { cassette_name: 'api_geo_all' } do
let(:procedure) { create(:procedure, :published, :for_individual, :with_all_champs) }
let(:service) { ProcedureExportService.new(procedure, procedure.dossiers) }
let(:memory_store) { ActiveSupport::Cache.lookup_store(:memory_store) }
before do
allow(APIGeoService).to receive(:departement_name).with('01').and_return('Ain')
allow(Rails).to receive(:cache).and_return(memory_store)
Rails.cache.clear
end
describe 'to_xlsx' do
@ -88,7 +92,10 @@ describe ProcedureExportService do
"dgfip",
"pole_emploi",
"mesri",
"text"
"text",
"epci",
"epci (Code)",
"epci (Département)"
]
end
@ -195,7 +202,10 @@ describe ProcedureExportService do
"dgfip",
"pole_emploi",
"mesri",
"text"
"text",
"epci",
"epci (Code)",
"epci (Département)"
]
end
@ -285,7 +295,10 @@ describe ProcedureExportService do
"dgfip",
"pole_emploi",
"mesri",
"text"
"text",
"epci",
"epci (Code)",
"epci (Département)"
]
end