feat(procedure): estimate fill duration takes account of libelle & description read time

Sometimes long description on types de champ
significantly increases global fill duration.

140 words per minute is a good estimate of attentive off-voice read time.
(This is independent of characters per word).

Closes #7963
This commit is contained in:
Colin Darie 2022-10-27 20:13:21 +02:00
parent c59867b456
commit 7731c6ad64
5 changed files with 56 additions and 17 deletions

View file

@ -204,7 +204,7 @@ class ProcedureRevision < ApplicationRecord
def compute_estimated_fill_duration
tdc_durations = types_de_champ_public.fillable.map do |tdc|
duration = tdc.estimated_fill_duration(self)
duration = tdc.estimated_fill_duration(self) + tdc.estimated_read_duration
tdc.mandatory ? duration : duration / 2
end
tdc_durations.sum

View file

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

View file

@ -8,11 +8,14 @@ class TypesDeChamp::RepetitionTypeDeChamp < TypesDeChamp::TypeDeChampBase
def estimated_fill_duration(revision)
estimated_rows_in_repetition = 2.5
estimated_row_duration = revision
.children_of(@type_de_champ)
.map { |child_tdc| child_tdc.estimated_fill_duration(revision) }
.sum
estimated_row_duration * estimated_rows_in_repetition
children = revision.children_of(@type_de_champ)
estimated_row_duration = children.map { _1.estimated_fill_duration(revision) }.sum
estimated_children_read_duration = children.map(&:estimated_read_duration).sum
# Count only once children read time for all rows
estimated_row_duration * estimated_rows_in_repetition + estimated_children_read_duration
end
# We have to truncate the label here as spreadsheets have a (30 char) limit on length.

View file

@ -3,9 +3,9 @@ class TypesDeChamp::TypeDeChampBase
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
READ_WORDS_PER_SECOND = 140.0 / 60 # 140 words per minute
def initialize(type_de_champ)
@type_de_champ = type_de_champ
@ -33,6 +33,16 @@ class TypesDeChamp::TypeDeChampBase
# May be overridden by subclasses.
def estimated_fill_duration(revision)
FILL_DURATION_SHORT
def estimated_read_duration
return 0.seconds if description.blank?
sanitizer = Rails::Html::Sanitizer.full_sanitizer.new
content = sanitizer.sanitize(description)
words = content.split(/\s+/).size
(words / READ_WORDS_PER_SECOND).round.seconds
end
def build_champ(params)

View file

@ -661,11 +661,14 @@ describe ProcedureRevision do
describe '#estimated_fill_duration' do
let(:mandatory) { true }
let(:description) { nil }
let(:description_read_time) { ((description || "").split.size / TypesDeChamp::TypeDeChampBase::READ_WORDS_PER_SECOND).round }
let(:types_de_champ_public) do
[
{ mandatory: true },
{ type: :siret, mandatory: true },
{ type: :piece_justificative, mandatory: mandatory }
{ mandatory: true, description: },
{ type: :siret, mandatory: true, description: },
{ type: :piece_justificative, mandatory:, description: }
]
end
let(:procedure) { create(:procedure, types_de_champ_public: types_de_champ_public) }
@ -676,7 +679,8 @@ describe ProcedureRevision do
expect(subject).to eq \
TypesDeChamp::TypeDeChampBase::FILL_DURATION_SHORT \
+ TypesDeChamp::TypeDeChampBase::FILL_DURATION_MEDIUM \
+ TypesDeChamp::TypeDeChampBase::FILL_DURATION_LONG
+ TypesDeChamp::TypeDeChampBase::FILL_DURATION_LONG \
+ 3 * description_read_time
end
context 'when some champs are optional' do
@ -684,9 +688,22 @@ describe ProcedureRevision do
it 'estimates that half of optional champs will be filled' do
expect(subject).to eq \
TypesDeChamp::TypeDeChampBase::FILL_DURATION_SHORT \
TypesDeChamp::TypeDeChampBase::FILL_DURATION_SHORT \
+ TypesDeChamp::TypeDeChampBase::FILL_DURATION_MEDIUM \
+ TypesDeChamp::TypeDeChampBase::FILL_DURATION_LONG / 2
+ 2 * description_read_time \
+ (description_read_time + TypesDeChamp::TypeDeChampBase::FILL_DURATION_LONG) / 2
end
end
context 'when some champs have a description' do
let(:description) { "some four words description" }
it 'estimates that duration includes description reading time' do
expect(subject).to eq \
TypesDeChamp::TypeDeChampBase::FILL_DURATION_SHORT \
+ TypesDeChamp::TypeDeChampBase::FILL_DURATION_MEDIUM \
+ TypesDeChamp::TypeDeChampBase::FILL_DURATION_LONG \
+ 3 * description_read_time
end
end
@ -696,17 +713,24 @@ describe ProcedureRevision do
{
type: :repetition,
mandatory: true,
description:,
children: [
{ mandatory: true },
{ type: :piece_justificative, position: 2, mandatory: true }
{ mandatory: true, description: "word " * 10 },
{ type: :piece_justificative, position: 2, mandatory: true, description: nil }
]
}
]
end
it 'estimates that between 2 and 3 rows will be filled for each repetition' do
repetable_block_read_duration = description_read_time
row_duration = TypesDeChamp::TypeDeChampBase::FILL_DURATION_SHORT + TypesDeChamp::TypeDeChampBase::FILL_DURATION_LONG
expect(subject).to eq row_duration * 2.5
children_read_duration = (10 / TypesDeChamp::TypeDeChampBase::READ_WORDS_PER_SECOND).round
expect(subject).to eq repetable_block_read_duration + row_duration * 2.5 + children_read_duration
end
end
end
end
@ -722,6 +746,7 @@ describe ProcedureRevision do
before do
draft_revision.estimated_fill_duration
draft_revision.types_de_champ.first.update!(type_champ: TypeDeChamp.type_champs.fetch(:piece_justificative))
draft_revision.reload
end
it 'returns an up-to-date estimate' do
@ -729,6 +754,7 @@ describe ProcedureRevision do
TypesDeChamp::TypeDeChampBase::FILL_DURATION_LONG \
+ TypesDeChamp::TypeDeChampBase::FILL_DURATION_MEDIUM \
+ TypesDeChamp::TypeDeChampBase::FILL_DURATION_LONG \
+ 3 * description_read_time
end
end