feat(demarche): describe procedure prefilling (#8187)

* feat(demarche): description

Show the description of an opendata procedure (published or draft),
with help about how to prefill a dossier for this procedure.

Co-authored-by: Damien Le Thiec <damien.lethiec@gmail.com>
This commit is contained in:
Sébastien Carceles 2022-12-19 12:32:09 +01:00 committed by GitHub
parent 936a1bfd91
commit 0a10a08c21
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 497 additions and 26 deletions

View file

@ -0,0 +1,13 @@
@import "colors";
.code-block {
background-color: $black;
color: $white;
border-radius: 3px;
padding: 2rem;
display: flex;
align-items: center;
font-weight: bold;
justify-content: center;
overflow: auto;
}

View file

@ -37,6 +37,7 @@
}
.confirmation-resend {
p,
label {
margin-bottom: $default-padding;

View file

@ -0,0 +1,32 @@
class PrefillDescriptionsController < ApplicationController
before_action :retrieve_procedure
before_action :set_prefill_description
def edit
end
def update
@prefill_description.update(prefill_description_params)
respond_to do |format|
format.turbo_stream
format.html { render :edit }
end
end
private
def retrieve_procedure
@procedure = Procedure.publiees_ou_brouillons.opendata.find_by!(path: params[:path])
end
def set_prefill_description
@prefill_description = PrefillDescription.new(@procedure)
end
def prefill_description_params
params.require(:procedure).permit(selected_type_de_champ_ids: [])
rescue ActionController::ParameterMissing
{ selected_type_de_champ_ids: [] }
end
end

View file

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

View file

@ -0,0 +1,46 @@
class PrefillDescription < SimpleDelegator
include Rails.application.routes.url_helpers
MAX_PREFILL_LINK_LENGTH = 2000
attr_reader :selected_type_de_champ_ids
def initialize(procedure)
super(procedure)
@selected_type_de_champ_ids = []
end
def update(attributes)
@selected_type_de_champ_ids = attributes[:selected_type_de_champ_ids].presence || []
end
def types_de_champ
active_revision.types_de_champ_public
end
def include?(type_de_champ_id)
selected_type_de_champ_ids.include?(type_de_champ_id.to_s)
end
def link_too_long?
prefill_link.length > MAX_PREFILL_LINK_LENGTH
end
def prefill_link
@prefill_link ||= commencer_url({ path: path }.merge(prefilled_champs_for_link))
end
def prefilled_champs
@prefilled_champs ||= types_de_champ.where(id: selected_type_de_champ_ids)
end
private
def prefilled_champs_for_link
prefilled_champs.map { |type_de_champ| ["champ_#{type_de_champ.to_typed_id}", type_de_champ.libelle] }.to_h
end
def prefilled_champs_for_query
prefilled_champs.map { |type_de_champ| "\"champ_#{type_de_champ.to_typed_id}\": \"#{type_de_champ.libelle}\"" } .join(', ')
end
end

View file

@ -31,16 +31,6 @@ class PrefillParams
end
class PrefillValue
AUTHORIZED_TYPES_DE_CHAMPS = [
TypeDeChamp.type_champs.fetch(:text),
TypeDeChamp.type_champs.fetch(:textarea),
TypeDeChamp.type_champs.fetch(:decimal_number),
TypeDeChamp.type_champs.fetch(:integer_number),
TypeDeChamp.type_champs.fetch(:email),
TypeDeChamp.type_champs.fetch(:phone),
TypeDeChamp.type_champs.fetch(:iban)
]
NEED_VALIDATION_TYPES_DE_CHAMPS = [
TypeDeChamp.type_champs.fetch(:decimal_number),
TypeDeChamp.type_champs.fetch(:integer_number)
@ -54,7 +44,7 @@ class PrefillParams
end
def prefillable?
authorized? && valid?
champ.prefillable? && valid?
end
def to_h
@ -66,10 +56,6 @@ class PrefillParams
private
def authorized?
AUTHORIZED_TYPES_DE_CHAMPS.include?(champ.type_champ)
end
def valid?
return true unless NEED_VALIDATION_TYPES_DE_CHAMPS.include?(champ.type_champ)

View file

@ -204,15 +204,16 @@ class Procedure < ApplicationRecord
has_one_attached :notice
has_one_attached :deliberation
scope :brouillons, -> { where(aasm_state: :brouillon) }
scope :publiees, -> { where(aasm_state: :publiee) }
scope :closes, -> { where(aasm_state: [:close, :depubliee]) }
scope :opendata, -> { where(opendata: true) }
scope :publiees_ou_closes, -> { where(aasm_state: [:publiee, :close, :depubliee]) }
scope :by_libelle, -> { order(libelle: :asc) }
scope :created_during, -> (range) { where(created_at: range) }
scope :cloned_from_library, -> { where(cloned_from_library: true) }
scope :declarative, -> { where.not(declarative_with_state: nil) }
scope :brouillons, -> { where(aasm_state: :brouillon) }
scope :publiees, -> { where(aasm_state: :publiee) }
scope :publiees_ou_brouillons, -> { publiees.or(brouillons) }
scope :closes, -> { where(aasm_state: [:close, :depubliee]) }
scope :opendata, -> { where(opendata: true) }
scope :publiees_ou_closes, -> { where(aasm_state: [:publiee, :close, :depubliee]) }
scope :by_libelle, -> { order(libelle: :asc) }
scope :created_during, -> (range) { where(created_at: range) }
scope :cloned_from_library, -> { where(cloned_from_library: true) }
scope :declarative, -> { where.not(declarative_with_state: nil) }
scope :discarded_expired, -> do
with_discarded

View file

@ -251,6 +251,18 @@ class TypeDeChamp < ApplicationRecord
collapsible_explanation_enabled == "1"
end
def prefillable?
type_champ.in?([
TypeDeChamp.type_champs.fetch(:text),
TypeDeChamp.type_champs.fetch(:textarea),
TypeDeChamp.type_champs.fetch(:decimal_number),
TypeDeChamp.type_champs.fetch(:integer_number),
TypeDeChamp.type_champs.fetch(:email),
TypeDeChamp.type_champs.fetch(:phone),
TypeDeChamp.type_champs.fetch(:iban)
])
end
def fillable?
!non_fillable?
end

View file

@ -0,0 +1,15 @@
= turbo_frame_tag "#{dom_id(prefill_description)}_url" do
- theme = prefill_description.link_too_long? ? :warning : :success
- icon = prefill_description.link_too_long? ? "fr-icon-warning-fill" : "fr-icon-paint-fill"
- body = prefill_description.link_too_long? ? t("views.prefill_descriptions.edit.prefill_link_too_long") : t("views.prefill_descriptions.edit.prefill_link_info")
- if prefill_description.prefilled_champs.any?
= render Dsfr::CalloutComponent.new(title: t("views.prefill_descriptions.edit.prefill_link_title"), theme: theme, icon: icon) do |c|
- c.with_body do
= body
%pre
%code.code-block
= prefill_description.prefill_link
- unless prefill_description.link_too_long?
- c.with_bottom do
= render Dsfr::CopyButtonComponent.new(title: t("views.prefill_descriptions.edit.prefill_link_copy"), text: prefill_description.prefill_link)

View file

@ -0,0 +1,44 @@
= turbo_frame_tag "#{dom_id(@prefill_description)}_types_de_champs" do
.card
.card-title
%span.icon.edit
= t("views.prefill_descriptions.edit.champs_title")
%table.table.hoverable
%thead
%tr
%th
= t("views.prefill_descriptions.edit.champ_id")
%th
= t("views.prefill_descriptions.edit.champ_type")
%th
= t("views.prefill_descriptions.edit.champ_libelle")
%th
= t("views.prefill_descriptions.edit.champ_description")
%th
= t("views.prefill_descriptions.edit.champ_prefill")
%tbody
- prefill_description.types_de_champ.each do |type_de_champ|
%tr
%td
= type_de_champ.to_typed_id
%td
= t("activerecord.attributes.type_de_champ.type_champs.#{type_de_champ.type_champ}")
%td
= type_de_champ.libelle
%td
= type_de_champ.description
%td.text-center
= form_for prefill_description, url: prefill_description_path(prefill_description.path), data: { turbo: true } do |f|
- if prefill_description.include?(type_de_champ.id)
- (prefill_description.selected_type_de_champ_ids - [type_de_champ.id.to_s]).each do |id|
= f.hidden_field :selected_type_de_champ_ids, value: id, multiple: true
= f.submit t("views.prefill_descriptions.edit.champ_remove"), class: 'fr-btn fr-btn--secondary fr-btn--md'
- elsif type_de_champ.prefillable?
- (prefill_description.selected_type_de_champ_ids + [type_de_champ.id.to_s]).each do |id|
= f.hidden_field :selected_type_de_champ_ids, value: id, multiple: true
= f.submit t("views.prefill_descriptions.edit.champ_add"), class: 'fr-btn fr-btn--md'
- else
%button.fr-btn.fr-btn--secondary{ disabled: true }
= t("views.prefill_descriptions.edit.champ_unavailable")

View file

@ -0,0 +1,19 @@
- content_for(:title, @prefill_description.libelle)
- content_for :footer do
= render partial: "root/footer"
.container
.two-columns.procedure-context
.columns-container
.column.procedure-preview
= render partial: 'shared/procedure_description', locals: { procedure: @prefill_description }
.column.procedure-context-content
%p
= t("views.prefill_descriptions.edit.intro_html", libelle: @prefill_description.libelle)
%p
= t("views.prefill_descriptions.edit.info")
= render "types_de_champs", prefill_description: @prefill_description
= render "prefill_link", prefill_description: @prefill_description

View file

@ -0,0 +1,5 @@
= turbo_stream.replace "#{dom_id(@prefill_description)}_types_de_champs" do
= render "types_de_champs", prefill_description: @prefill_description
= turbo_stream.replace "#{dom_id(@prefill_description)}_url" do
= render "prefill_link", prefill_description: @prefill_description

View file

@ -101,6 +101,23 @@ en:
show_my_submitted_file: 'Show my submitted file'
want_empty_pdf: "You prefer to submit a paper form? You can download an empty PDF file, and send it to the right administration : %{service} - %{adresse}"
download_empty_pdf: 'Download an empty PDF file'
prefill_descriptions:
edit:
intro_html: "You'd like to allow your users to <strong>create a prefilled file</strong>, with data you already have, for the procedure « %{libelle} »."
info: Add the fields you want to prefill, thanks to the table below. Once it's over, copy the prefill link, replace the values and share the link.
champs_title: Fields
champ_id: ID
champ_type: Type
champ_libelle: Label
champ_description: Description
champ_prefill: Prefillable
champ_add: Add
champ_remove: Remove
champ_unavailable: Unavailable
prefill_link_title: Prefill link (GET)
prefill_link_info: Use the button to copy the link, then remplace the values with your data.
prefill_link_too_long: Warning, the prefill link is too long and may not work on all browsers.
prefill_link_copy: Copy prefill link
registrations:
new:
title: "Create an account %{name}"

View file

@ -91,6 +91,23 @@ fr:
show_my_submitted_file: 'Voir mon dossier déposé'
want_empty_pdf: "Vous souhaitez effectuer une demande par papier ? Vous pouvez télécharger un dossier vide au format PDF, et lenvoyer à ladministration concernée : %{service} - %{adresse}"
download_empty_pdf: 'Télécharger un dossier vide au format PDF'
prefill_descriptions:
edit:
intro_html: "Vous souhaitez permettre à vos usager·ères la <strong>création d'un dossier prérempli</strong>, à partir de données dont vous disposez déjà, pour la démarche « %{libelle} »."
info: Pour cela, ajoutez les champs que vous souhaitez préremplir, grâce au tableau ci-dessous. Lorsque vous avez terminé, il ne vous reste plus qu'à copier le lien de préremplissage, à remplacer les valeurs et à le partager.
champs_title: Champs de la démarche
champ_id: ID
champ_type: Type
champ_libelle: Libellé
champ_description: Description
champ_prefill: Préremplissable
champ_add: Ajouter
champ_remove: Retirer
champ_unavailable: Indisponible
prefill_link_title: Lien de préremplissage (GET)
prefill_link_info: Copiez le lien grâce au bouton ci-dessous et remplacez les valeurs par les données dont vous disposez.
prefill_link_too_long: Attention, ce lien de préremplissage est trop long et risque de ne pas fonctionner sur certains navigateurs.
prefill_link_copy: Copier le lien de préremplissage
registrations:
new:
title: "Créez-vous un compte %{name}"
@ -99,8 +116,6 @@ fr:
wanna_say: 'Voulez-vous dire'
password_label: "Mot de passe (%{min_length} caractères minimum)"
password_placeholder: "%{min_length} caractères minimum"
invites:
dropdown:
invite_to_edit: Inviter une personne à modifier ce dossier

View file

@ -189,6 +189,13 @@ Rails.application.routes.draw do
post "webhooks/helpscout_support_dev", to: "webhook#helpscout_support_dev"
match "webhooks/helpscout", to: lambda { |_| [204, {}, nil] }, via: :head
get '/preremplir/:path', to: 'prefill_descriptions#edit'
resources :procedures, only: [], param: :path do
member do
resource :prefill_description, only: :update
end
end
#
# Deprecated UI
#
@ -251,6 +258,12 @@ Rails.application.routes.draw do
end
resources :pays, only: :index
namespace :public do
namespace :v1 do
resources :dossiers, only: :create
end
end
end
#

View file

@ -0,0 +1,100 @@
describe PrefillDescriptionsController, type: :controller do
describe '#edit' do
subject(:edit_request) do
get :edit, params: { path: procedure.path }
end
context 'when the procedure is found' do
context 'when the procedure is publiee' do
context 'when the procedure is opendata' do
let(:procedure) { create(:procedure, :published, opendata: true) }
it { expect(edit_request).to render_template(:edit) }
end
context 'when the procedure is not opendata' do
let(:procedure) { create(:procedure, :published, opendata: false) }
it { expect { edit_request }.to raise_error(ActiveRecord::RecordNotFound) }
end
end
context 'when the procedure is brouillon' do
context 'when the procedure is opendata' do
let(:procedure) { create(:procedure, :draft, opendata: true) }
it { expect(edit_request).to render_template(:edit) }
end
context 'when the procedure is not opendata' do
let(:procedure) { create(:procedure, :draft, opendata: false) }
it { expect { edit_request }.to raise_error(ActiveRecord::RecordNotFound) }
end
end
context 'when the procedure is not publiee and not brouillon' do
let(:procedure) { create(:procedure, :closed) }
it { expect { edit_request }.to raise_error(ActiveRecord::RecordNotFound) }
end
end
context 'when the procedure is not found' do
let(:procedure) { double(Procedure, path: "wrong path") }
it { expect { edit_request }.to raise_error(ActiveRecord::RecordNotFound) }
end
end
describe "#update" do
render_views
let(:procedure) { create(:procedure, :published, opendata: true) }
let(:type_de_champ) { create(:type_de_champ_text, procedure: procedure) }
let(:type_de_champ2) { create(:type_de_champ_text, procedure: procedure) }
subject(:update_request) do
patch :update, params: { path: procedure.path, procedure: params }, format: :turbo_stream
end
before { update_request }
context 'when adding a type_de_champ_id' do
let(:type_de_champ_to_add) { create(:type_de_champ_text, procedure: procedure) }
let(:params) { { selected_type_de_champ_ids: [type_de_champ.id, type_de_champ_to_add.id] } }
it { expect(response).to render_template(:update) }
it "includes the prefill URL" do
expect(response.body).to include(commencer_path(path: procedure.path))
expect(response.body).to include({ "champ_#{type_de_champ.to_typed_id}" => type_de_champ.libelle }.to_query)
expect(response.body).to include({ "champ_#{type_de_champ_to_add.to_typed_id}" => type_de_champ_to_add.libelle }.to_query)
end
end
context 'when removing a type_de_champ_id' do
let(:type_de_champ_to_remove) { type_de_champ2 }
let(:params) { { selected_type_de_champ_ids: [type_de_champ] } }
it { expect(response).to render_template(:update) }
it "includes the prefill URL" do
expect(response.body).to include(commencer_path(path: procedure.path))
expect(response.body).to include({ "champ_#{type_de_champ.to_typed_id}" => type_de_champ.libelle }.to_query)
expect(response.body).not_to include({ "champ_#{type_de_champ_to_remove.to_typed_id}" => type_de_champ_to_remove.libelle }.to_query)
end
end
context 'when removing the last type de champ' do
let(:type_de_champ_to_remove) { type_de_champ }
let(:params) { { selected_type_de_champ_ids: [] } }
it { expect(response).to render_template(:update) }
it "does not include the prefill URL" do
expect(response.body).not_to include(commencer_path(path: procedure.path))
end
end
end
end

View file

@ -279,6 +279,10 @@ FactoryBot.define do
end
end
trait :draft do
aasm_state { :brouillon }
end
trait :published do
aasm_state { :publiee }
path { generate(:published_path) }

View file

@ -0,0 +1,79 @@
RSpec.describe PrefillDescription, type: :model do
include Rails.application.routes.url_helpers
describe '#update' do
let(:prefill_description) { described_class.new(build(:procedure)) }
let(:selected_type_de_champ_ids) { ["1", "2"] }
subject(:update) { prefill_description.update(attributes) }
context 'when selected_type_de_champ_ids are given' do
let(:attributes) { { selected_type_de_champ_ids: selected_type_de_champ_ids } }
it 'populate selected_type_de_champ_ids' do
expect { update }.to change { prefill_description.selected_type_de_champ_ids }.from([]).to(selected_type_de_champ_ids)
end
end
end
describe '#types_de_champ' do
let(:procedure) { create(:procedure) }
let(:type_de_champ) { create(:type_de_champ_text, procedure: procedure) }
let(:prefill_description) { described_class.new(procedure) }
it { expect(prefill_description.types_de_champ).to match([type_de_champ]) }
end
describe '#include?' do
let(:prefill_description) { described_class.new(build(:procedure)) }
let(:type_de_champ_id) { 1 }
subject(:included) { prefill_description.include?(type_de_champ_id) }
context 'when the id has been added to the prefill_description' do
before { prefill_description.update(selected_type_de_champ_ids: ["1"]) }
it { expect(included).to eq(true) }
end
context 'when the id has not be added to the prefill_description' do
it { expect(included).to eq(false) }
end
end
describe '#link_too_long?' do
let(:procedure) { create(:procedure) }
let(:prefill_description) { described_class.new(procedure) }
subject(:too_long) { prefill_description.link_too_long? }
before { prefill_description.update(selected_type_de_champ_ids: create_list(:type_de_champ_text, type_de_champs_count, procedure: procedure).map(&:id)) }
context 'when the prefill link is too long' do
let(:type_de_champs_count) { 60 }
it { expect(too_long).to eq(true) }
end
context 'when the prefill link is not too long' do
let(:type_de_champs_count) { 2 }
it { expect(too_long).to eq(false) }
end
end
describe '#prefill_link' do
let(:procedure) { create(:procedure) }
let(:type_de_champ) { create(:type_de_champ_text, procedure: procedure) }
let(:prefill_description) { described_class.new(procedure) }
before { prefill_description.update(selected_type_de_champ_ids: [type_de_champ.id]) }
it "builds the URL to create a new prefilled dossier" do
expect(prefill_description.prefill_link).to eq(
commencer_url(
path: procedure.path,
"champ_#{type_de_champ.to_typed_id}" => type_de_champ.libelle
)
)
end
end
end

View file

@ -225,4 +225,51 @@ describe TypeDeChamp do
expect(type_de_champ.condition).to eq(condition)
end
end
describe '#prefillable?' do
shared_examples 'a prefillable type de champ' do |factory|
it { expect(build(factory).prefillable?).to eq(true) }
end
shared_examples 'a non-prefillable type de champ' do |factory|
it { expect(build(factory).prefillable?).to eq(false) }
end
it_behaves_like "a prefillable type de champ", :type_de_champ_text
it_behaves_like "a prefillable type de champ", :type_de_champ_textarea
it_behaves_like "a prefillable type de champ", :type_de_champ_decimal_number
it_behaves_like "a prefillable type de champ", :type_de_champ_integer_number
it_behaves_like "a prefillable type de champ", :type_de_champ_email
it_behaves_like "a prefillable type de champ", :type_de_champ_phone
it_behaves_like "a prefillable type de champ", :type_de_champ_iban
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_checkbox
it_behaves_like "a non-prefillable type de champ", :type_de_champ_civilite
it_behaves_like "a non-prefillable type de champ", :type_de_champ_yes_no
it_behaves_like "a non-prefillable type de champ", :type_de_champ_date
it_behaves_like "a non-prefillable type de champ", :type_de_champ_datetime
it_behaves_like "a non-prefillable type de champ", :type_de_champ_drop_down_list
it_behaves_like "a non-prefillable type de champ", :type_de_champ_multiple_drop_down_list
it_behaves_like "a non-prefillable type de champ", :type_de_champ_linked_drop_down_list
it_behaves_like "a non-prefillable type de champ", :type_de_champ_header_section
it_behaves_like "a non-prefillable type de champ", :type_de_champ_explication
it_behaves_like "a non-prefillable type de champ", :type_de_champ_piece_justificative
it_behaves_like "a non-prefillable type de champ", :type_de_champ_repetition
it_behaves_like "a non-prefillable type de champ", :type_de_champ_cnaf
it_behaves_like "a non-prefillable type de champ", :type_de_champ_dgfip
it_behaves_like "a non-prefillable type de champ", :type_de_champ_pole_emploi
it_behaves_like "a non-prefillable type de champ", :type_de_champ_mesri
it_behaves_like "a non-prefillable type de champ", :type_de_champ_carte
it_behaves_like "a non-prefillable type de champ", :type_de_champ_address
it_behaves_like "a non-prefillable type de champ", :type_de_champ_pays
it_behaves_like "a non-prefillable type de champ", :type_de_champ_regions
it_behaves_like "a non-prefillable type de champ", :type_de_champ_departements
it_behaves_like "a non-prefillable type de champ", :type_de_champ_siret
it_behaves_like "a non-prefillable type de champ", :type_de_champ_rna
it_behaves_like "a non-prefillable type de champ", :type_de_champ_annuaire_education
end
end

View file

@ -0,0 +1,21 @@
describe 'As an integrator:', js: true do
let(:procedure) { create(:procedure, :published, opendata: true) }
let!(:type_de_champ) { create(:type_de_champ_text, procedure: procedure) }
before { visit "/preremplir/#{procedure.path}" }
scenario 'I can read the procedure prefilling (aka public champs)' do
expect(page).to have_content(type_de_champ.to_typed_id)
expect(page).to have_content(I18n.t("activerecord.attributes.type_de_champ.type_champs.#{type_de_champ.type_champ}"))
expect(page).to have_content(type_de_champ.libelle)
expect(page).to have_content(type_de_champ.description)
end
scenario 'I can select champs to prefill' do
click_on 'Ajouter'
prefill_description = PrefillDescription.new(procedure)
prefill_description.update(selected_type_de_champ_ids: [type_de_champ.id.to_s])
expect(page).to have_content(prefill_description.prefill_link)
end
end