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:
commit
cd5ef9b202
25 changed files with 217 additions and 17 deletions
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 }
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -1,2 +1,5 @@
|
|||
class TypesDeChamp::AnnuaireEducationTypeDeChamp < TypesDeChamp::TextTypeDeChamp
|
||||
def estimated_fill_duration(revision)
|
||||
FILL_DURATION_MEDIUM
|
||||
end
|
||||
end
|
||||
|
|
|
@ -11,4 +11,8 @@ class TypesDeChamp::CarteTypeDeChamp < TypesDeChamp::TypeDeChampBase
|
|||
:znieff,
|
||||
:cadastres
|
||||
]
|
||||
|
||||
def estimated_fill_duration(revision)
|
||||
FILL_DURATION_LONG
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,2 +1,5 @@
|
|||
class TypesDeChamp::CnafTypeDeChamp < TypesDeChamp::TextTypeDeChamp
|
||||
def estimated_fill_duration(revision)
|
||||
FILL_DURATION_MEDIUM
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,2 +1,5 @@
|
|||
class TypesDeChamp::DgfipTypeDeChamp < TypesDeChamp::TextTypeDeChamp
|
||||
def estimated_fill_duration(revision)
|
||||
FILL_DURATION_MEDIUM
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,2 +1,5 @@
|
|||
class TypesDeChamp::IbanTypeDeChamp < TypesDeChamp::TypeDeChampBase
|
||||
def estimated_fill_duration(revision)
|
||||
FILL_DURATION_MEDIUM
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,2 +1,5 @@
|
|||
class TypesDeChamp::MesriTypeDeChamp < TypesDeChamp::TextTypeDeChamp
|
||||
def estimated_fill_duration(revision)
|
||||
FILL_DURATION_MEDIUM
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,2 +1,5 @@
|
|||
class TypesDeChamp::PieceJustificativeTypeDeChamp < TypesDeChamp::TypeDeChampBase
|
||||
def estimated_fill_duration(revision)
|
||||
FILL_DURATION_LONG
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,2 +1,5 @@
|
|||
class TypesDeChamp::PoleEmploiTypeDeChamp < TypesDeChamp::TextTypeDeChamp
|
||||
def estimated_fill_duration(revision)
|
||||
FILL_DURATION_MEDIUM
|
||||
end
|
||||
end
|
||||
|
|
|
@ -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}"
|
||||
|
|
|
@ -1,2 +1,5 @@
|
|||
class TypesDeChamp::SiretTypeDeChamp < TypesDeChamp::TypeDeChampBase
|
||||
def estimated_fill_duration(revision)
|
||||
FILL_DURATION_MEDIUM
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,2 +1,5 @@
|
|||
class TypesDeChamp::TextareaTypeDeChamp < TypesDeChamp::TextTypeDeChamp
|
||||
def estimated_fill_duration(revision)
|
||||
FILL_DURATION_MEDIUM
|
||||
end
|
||||
end
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 jusqu’au #{procedure_auto_archive_datetime(procedure)}.
|
||||
|
|
|
@ -26,6 +26,7 @@ features = [
|
|||
:hide_instructeur_email,
|
||||
:procedure_revisions,
|
||||
:procedure_routage_api,
|
||||
:procedure_estimated_fill_duration,
|
||||
:procedure_process_expired_dossiers_termine
|
||||
]
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue