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:
parent
c59867b456
commit
7731c6ad64
5 changed files with 56 additions and 17 deletions
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
Loading…
Reference in a new issue