Merge pull request #7352 from betagouv/estimated-completion-time

Usager : affichage du temps estimé pour remplir la démarche (derrière un feature flag)
This commit is contained in:
Pierre de La Morinerie 2022-05-25 10:46:13 +02:00 committed by GitHub
commit cd5ef9b202
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
25 changed files with 217 additions and 17 deletions

View file

@ -44,15 +44,13 @@ $procedure-description-line-height: 22px;
}
}
.procedure-auto-archive {
cursor: pointer;
.procedure-configuration {
font-size: 20px;
margin-bottom: 32px;
}
p {
padding-top: 8px;
font-size: 16px;
}
.procedure-configuration--auto-archive {
cursor: pointer;
summary {
font-size: 20px;
@ -64,6 +62,11 @@ $procedure-description-line-height: 22px;
display: none;
}
}
p {
padding-top: 8px;
font-size: 16px;
}
}
.procedure-auto-archive-title {

View file

@ -82,4 +82,10 @@ module ProcedureHelper
return "//#{uri}" if uri.scheme.nil?
uri.to_s
end
def estimated_fill_duration_minutes(procedure)
seconds = procedure.active_revision.estimated_fill_duration
minutes = (seconds / 60.0).round
[1, minutes].max
end
end

View file

@ -180,8 +180,25 @@ class ProcedureRevision < ApplicationRecord
tdcs_as_json
end
# Estimated duration to fill the form, in seconds.
#
# If the revision is locked (i.e. published), the result is cached (because type de champs can no longer be mutated).
def estimated_fill_duration
Rails.cache.fetch("#{cache_key_with_version}/estimated_fill_duration", expires_in: 12.hours, force: !locked?) do
compute_estimated_fill_duration
end
end
private
def compute_estimated_fill_duration
tdc_durations = types_de_champ_public.fillable.map do |tdc|
duration = tdc.estimated_fill_duration(self)
tdc.mandatory ? duration : duration / 2
end
tdc_durations.sum
end
def children_types_de_champ_as_json(tdcs_as_json, parent_tdcs)
parent_tdcs.each do |parent_tdc|
tdc_as_json = tdcs_as_json.find { |json| json["id"] == parent_tdc.stable_id }

View file

@ -66,7 +66,7 @@ class TypeDeChamp < ApplicationRecord
has_one :revision, through: :revision_type_de_champ
has_one :procedure, through: :revision
delegate :tags_for_template, :libelle_for_export, to: :dynamic_type
delegate :estimated_fill_duration, :tags_for_template, :libelle_for_export, to: :dynamic_type
class WithIndifferentAccess
def self.load(options)

View file

@ -1,2 +1,5 @@
class TypesDeChamp::AnnuaireEducationTypeDeChamp < TypesDeChamp::TextTypeDeChamp
def estimated_fill_duration(revision)
FILL_DURATION_MEDIUM
end
end

View file

@ -11,4 +11,8 @@ class TypesDeChamp::CarteTypeDeChamp < TypesDeChamp::TypeDeChampBase
:znieff,
:cadastres
]
def estimated_fill_duration(revision)
FILL_DURATION_LONG
end
end

View file

@ -1,2 +1,5 @@
class TypesDeChamp::CnafTypeDeChamp < TypesDeChamp::TextTypeDeChamp
def estimated_fill_duration(revision)
FILL_DURATION_MEDIUM
end
end

View file

@ -1,2 +1,5 @@
class TypesDeChamp::DgfipTypeDeChamp < TypesDeChamp::TextTypeDeChamp
def estimated_fill_duration(revision)
FILL_DURATION_MEDIUM
end
end

View file

@ -1,2 +1,5 @@
class TypesDeChamp::IbanTypeDeChamp < TypesDeChamp::TypeDeChampBase
def estimated_fill_duration(revision)
FILL_DURATION_MEDIUM
end
end

View file

@ -1,2 +1,5 @@
class TypesDeChamp::MesriTypeDeChamp < TypesDeChamp::TextTypeDeChamp
def estimated_fill_duration(revision)
FILL_DURATION_MEDIUM
end
end

View file

@ -1,2 +1,5 @@
class TypesDeChamp::PieceJustificativeTypeDeChamp < TypesDeChamp::TypeDeChampBase
def estimated_fill_duration(revision)
FILL_DURATION_LONG
end
end

View file

@ -1,2 +1,5 @@
class TypesDeChamp::PoleEmploiTypeDeChamp < TypesDeChamp::TextTypeDeChamp
def estimated_fill_duration(revision)
FILL_DURATION_MEDIUM
end
end

View file

@ -6,6 +6,15 @@ class TypesDeChamp::RepetitionTypeDeChamp < TypesDeChamp::TypeDeChampBase
champ
end
def estimated_fill_duration(revision)
estimated_rows_in_repetition = 2.5
estimated_row_duration = @type_de_champ
.types_de_champ
.map { |child_tdc| child_tdc.estimated_fill_duration(revision) }
.sum
estimated_row_duration * estimated_rows_in_repetition
end
# We have to truncate the label here as spreadsheets have a (30 char) limit on length.
def libelle_for_export(index = 0)
str = "(#{stable_id}) #{libelle}"

View file

@ -1,2 +1,5 @@
class TypesDeChamp::SiretTypeDeChamp < TypesDeChamp::TypeDeChampBase
def estimated_fill_duration(revision)
FILL_DURATION_MEDIUM
end
end

View file

@ -1,2 +1,5 @@
class TypesDeChamp::TextareaTypeDeChamp < TypesDeChamp::TextTypeDeChamp
def estimated_fill_duration(revision)
FILL_DURATION_MEDIUM
end
end

View file

@ -1,4 +1,8 @@
class TypesDeChamp::TitreIdentiteTypeDeChamp < TypesDeChamp::TypeDeChampBase
FRANCE_CONNECT = 'france_connect'
PIECE_JUSTIFICATIVE = 'piece_justificative'
def estimated_fill_duration(revision)
FILL_DURATION_LONG
end
end

View file

@ -1,7 +1,11 @@
class TypesDeChamp::TypeDeChampBase
include ActiveModel::Validations
delegate :description, :libelle, :stable_id, to: :@type_de_champ
delegate :description, :libelle, :mandatory, :stable_id, to: :@type_de_champ
FILL_DURATION_SHORT = 10.seconds.in_seconds
FILL_DURATION_MEDIUM = 1.minute.in_seconds
FILL_DURATION_LONG = 3.minutes.in_seconds
def initialize(type_de_champ)
@type_de_champ = type_de_champ
@ -25,6 +29,12 @@ class TypesDeChamp::TypeDeChampBase
libelle
end
# Default estimated duration to fill the champ in a form, in seconds.
# May be overridden by subclasses.
def estimated_fill_duration(revision)
FILL_DURATION_SHORT
end
def build_champ(params)
@type_de_champ.champ.build(params)
end

View file

@ -1,6 +1,6 @@
= render partial: 'administrateurs/breadcrumbs',
locals: { steps: [link_to('Démarches', admin_procedures_path),
link_to(@procedure.libelle, admin_procedure_path(@procedure)),
link_to(@procedure.libelle.truncate_words(10), admin_procedure_path(@procedure)),
'Configuration des champs'], preview: true }
.container

View file

@ -8,10 +8,15 @@
%h1.procedure-title
= procedure.libelle
- if procedure.feature_enabled?(:procedure_estimated_fill_duration)
%p.procedure-configuration.procedure-configuration--fill-duration
%span.icon.clock
= t('shared.procedure_description.estimated_fill_duration', estimated_minutes: estimated_fill_duration_minutes(procedure))
- if procedure.auto_archive_on
%details.procedure-auto-archive
%details.procedure-configuration.procedure-configuration--auto-archive
%summary
%span.icon.clock
%span.icon.edit
%span.procedure-auto-archive-title Date limite : #{procedure_auto_archive_date(procedure)}
%p Vous pouvez déposer vos dossiers jusquau #{procedure_auto_archive_datetime(procedure)}.

View file

@ -26,6 +26,7 @@ features = [
:hide_instructeur_email,
:procedure_revisions,
:procedure_routage_api,
:procedure_estimated_fill_duration,
:procedure_process_expired_dossiers_termine
]

View file

@ -524,3 +524,5 @@ en:
dossiers_count: "Nb files"
weekly_distribution: "Weekly distribution"
weekly_distribution_details: "in the last 6 months"
procedure_description:
estimated_fill_duration: "Estimated fill time: %{estimated_minutes} mn"

View file

@ -575,3 +575,5 @@ fr:
dossiers_count: "Nb dossiers"
weekly_distribution: "Répartition par semaine"
weekly_distribution_details: "au cours des 6 derniers mois"
procedure_description:
estimated_fill_duration: "Temps de remplissage estimé : %{estimated_minutes} mn"

View file

@ -1,10 +1,32 @@
RSpec.describe ProcedureHelper, type: :helper do
let(:auto_archive_date) { Time.zone.local(2020, 8, 2, 12, 00) }
let(:procedure) { build(:procedure, auto_archive_on: auto_archive_date) }
describe '#procedure_auto_archive_datetime' do
let(:auto_archive_date) { Time.zone.local(2020, 8, 2, 12, 00) }
let(:procedure) { build(:procedure, auto_archive_on: auto_archive_date) }
subject { procedure_auto_archive_datetime(procedure) }
subject { procedure_auto_archive_datetime(procedure) }
it "displays the day before the auto archive date (to account for the '23h59' ending time)" do
expect(subject).to have_text("1 août 2020 à 23 h 59 (heure de Paris)")
it "displays the day before the auto archive date (to account for the '23h59' ending time)" do
expect(subject).to have_text("1 août 2020 à 23 h 59 (heure de Paris)")
end
end
describe '#estimated_fill_duration_minutes' do
subject { estimated_fill_duration_minutes(procedure) }
context 'with champs' do
let(:procedure) { build(:procedure, :with_yes_no, :with_piece_justificative) }
it 'rounds up the duration to the minute' do
expect(subject).to eq(2)
end
end
context 'without champs' do
let(:procedure) { build(:procedure) }
it 'never displays zero minutes' do
expect(subject).to eq(1)
end
end
end
end

View file

@ -545,4 +545,85 @@ describe ProcedureRevision do
end
end
end
describe '#estimated_fill_duration' do
let(:mandatory) { true }
let(:types_de_champ) do
[
build(:type_de_champ_text, position: 1, mandatory: true),
build(:type_de_champ_siret, position: 2, mandatory: true),
build(:type_de_champ_piece_justificative, position: 3, mandatory: mandatory)
]
end
let(:procedure) { create(:procedure, types_de_champ: types_de_champ) }
subject { procedure.active_revision.estimated_fill_duration }
it 'sums the durations of public champs' do
expect(subject).to eq \
TypesDeChamp::TypeDeChampBase::FILL_DURATION_SHORT \
+ TypesDeChamp::TypeDeChampBase::FILL_DURATION_MEDIUM \
+ TypesDeChamp::TypeDeChampBase::FILL_DURATION_LONG
end
context 'when some champs are optional' do
let(:mandatory) { false }
it 'estimates that half of optional champs will be filled' do
expect(subject).to eq \
TypesDeChamp::TypeDeChampBase::FILL_DURATION_SHORT \
+ TypesDeChamp::TypeDeChampBase::FILL_DURATION_MEDIUM \
+ TypesDeChamp::TypeDeChampBase::FILL_DURATION_LONG / 2
end
end
context 'when there are repetitions' do
let(:types_de_champ) do
[
build(:type_de_champ_repetition, position: 1, mandatory: true, types_de_champ: [
build(:type_de_champ_text, position: 1, mandatory: true),
build(:type_de_champ_piece_justificative, position: 2, mandatory: true)
])
]
end
it 'estimates that between 2 and 3 rows will be filled for each repetition' do
row_duration = TypesDeChamp::TypeDeChampBase::FILL_DURATION_SHORT + TypesDeChamp::TypeDeChampBase::FILL_DURATION_LONG
expect(subject).to eq row_duration * 2.5
end
end
describe 'caching behavior' do
let(:procedure) { create(:procedure, :published, types_de_champ: types_de_champ) }
before { Rails.cache = ActiveSupport::Cache::MemoryStore.new }
after { Rails.cache = ActiveSupport::Cache::NullStore.new }
context 'when a type de champ belonging to a draft revision is updated' do
let(:draft_revision) { procedure.draft_revision }
before do
draft_revision.estimated_fill_duration
draft_revision.types_de_champ.first.update!(type_champ: TypeDeChamp.type_champs.fetch(:piece_justificative))
end
it 'returns an up-to-date estimate' do
expect(draft_revision.estimated_fill_duration).to eq \
TypesDeChamp::TypeDeChampBase::FILL_DURATION_LONG \
+ TypesDeChamp::TypeDeChampBase::FILL_DURATION_MEDIUM \
+ TypesDeChamp::TypeDeChampBase::FILL_DURATION_LONG \
end
end
context 'when the revision is published (and thus immutable)' do
let(:published_revision) { procedure.published_revision }
it 'caches the estimate' do
expect(published_revision).to receive(:compute_estimated_fill_duration).once
published_revision.estimated_fill_duration
published_revision.estimated_fill_duration
end
end
end
end
end

View file

@ -22,4 +22,11 @@ describe 'shared/_procedure_description.html.haml', type: :view do
expect(rendered).to have_text('Date limite')
end
end
context 'when the procedure_estimated_fill_duration feature is enabled' do
before { Flipper.enable(:procedure_estimated_fill_duration) }
after { Flipper.disable(:procedure_estimated_fill_duration) }
it { is_expected.to have_text('Temps de remplissage estimé') }
end
end