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
|
def compute_estimated_fill_duration
|
||||||
tdc_durations = types_de_champ_public.fillable.map do |tdc|
|
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
|
tdc.mandatory ? duration : duration / 2
|
||||||
end
|
end
|
||||||
tdc_durations.sum
|
tdc_durations.sum
|
||||||
|
|
|
@ -113,7 +113,7 @@ class TypeDeChamp < ApplicationRecord
|
||||||
has_one :revision, through: :revision_type_de_champ
|
has_one :revision, through: :revision_type_de_champ
|
||||||
has_one :procedure, through: :revision
|
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
|
class WithIndifferentAccess
|
||||||
def self.load(options)
|
def self.load(options)
|
||||||
|
|
|
@ -8,11 +8,14 @@ class TypesDeChamp::RepetitionTypeDeChamp < TypesDeChamp::TypeDeChampBase
|
||||||
|
|
||||||
def estimated_fill_duration(revision)
|
def estimated_fill_duration(revision)
|
||||||
estimated_rows_in_repetition = 2.5
|
estimated_rows_in_repetition = 2.5
|
||||||
estimated_row_duration = revision
|
|
||||||
.children_of(@type_de_champ)
|
children = revision.children_of(@type_de_champ)
|
||||||
.map { |child_tdc| child_tdc.estimated_fill_duration(revision) }
|
|
||||||
.sum
|
estimated_row_duration = children.map { _1.estimated_fill_duration(revision) }.sum
|
||||||
estimated_row_duration * estimated_rows_in_repetition
|
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
|
end
|
||||||
|
|
||||||
# We have to truncate the label here as spreadsheets have a (30 char) limit on length.
|
# 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
|
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_MEDIUM = 1.minute.in_seconds
|
||||||
FILL_DURATION_LONG = 3.minutes.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)
|
def initialize(type_de_champ)
|
||||||
@type_de_champ = type_de_champ
|
@type_de_champ = type_de_champ
|
||||||
|
@ -33,6 +33,16 @@ class TypesDeChamp::TypeDeChampBase
|
||||||
# May be overridden by subclasses.
|
# May be overridden by subclasses.
|
||||||
def estimated_fill_duration(revision)
|
def estimated_fill_duration(revision)
|
||||||
FILL_DURATION_SHORT
|
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
|
end
|
||||||
|
|
||||||
def build_champ(params)
|
def build_champ(params)
|
||||||
|
|
|
@ -661,11 +661,14 @@ describe ProcedureRevision do
|
||||||
|
|
||||||
describe '#estimated_fill_duration' do
|
describe '#estimated_fill_duration' do
|
||||||
let(:mandatory) { true }
|
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
|
let(:types_de_champ_public) do
|
||||||
[
|
[
|
||||||
{ mandatory: true },
|
{ mandatory: true, description: },
|
||||||
{ type: :siret, mandatory: true },
|
{ type: :siret, mandatory: true, description: },
|
||||||
{ type: :piece_justificative, mandatory: mandatory }
|
{ type: :piece_justificative, mandatory:, description: }
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
let(:procedure) { create(:procedure, types_de_champ_public: types_de_champ_public) }
|
let(:procedure) { create(:procedure, types_de_champ_public: types_de_champ_public) }
|
||||||
|
@ -676,7 +679,8 @@ describe ProcedureRevision do
|
||||||
expect(subject).to eq \
|
expect(subject).to eq \
|
||||||
TypesDeChamp::TypeDeChampBase::FILL_DURATION_SHORT \
|
TypesDeChamp::TypeDeChampBase::FILL_DURATION_SHORT \
|
||||||
+ TypesDeChamp::TypeDeChampBase::FILL_DURATION_MEDIUM \
|
+ TypesDeChamp::TypeDeChampBase::FILL_DURATION_MEDIUM \
|
||||||
+ TypesDeChamp::TypeDeChampBase::FILL_DURATION_LONG
|
+ TypesDeChamp::TypeDeChampBase::FILL_DURATION_LONG \
|
||||||
|
+ 3 * description_read_time
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when some champs are optional' do
|
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
|
it 'estimates that half of optional champs will be filled' do
|
||||||
expect(subject).to eq \
|
expect(subject).to eq \
|
||||||
TypesDeChamp::TypeDeChampBase::FILL_DURATION_SHORT \
|
TypesDeChamp::TypeDeChampBase::FILL_DURATION_SHORT \
|
||||||
+ TypesDeChamp::TypeDeChampBase::FILL_DURATION_MEDIUM \
|
+ 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
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -696,17 +713,24 @@ describe ProcedureRevision do
|
||||||
{
|
{
|
||||||
type: :repetition,
|
type: :repetition,
|
||||||
mandatory: true,
|
mandatory: true,
|
||||||
|
description:,
|
||||||
children: [
|
children: [
|
||||||
{ mandatory: true },
|
{ mandatory: true, description: "word " * 10 },
|
||||||
{ type: :piece_justificative, position: 2, mandatory: true }
|
{ type: :piece_justificative, position: 2, mandatory: true, description: nil }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'estimates that between 2 and 3 rows will be filled for each repetition' do
|
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
|
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
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -722,6 +746,7 @@ describe ProcedureRevision do
|
||||||
before do
|
before do
|
||||||
draft_revision.estimated_fill_duration
|
draft_revision.estimated_fill_duration
|
||||||
draft_revision.types_de_champ.first.update!(type_champ: TypeDeChamp.type_champs.fetch(:piece_justificative))
|
draft_revision.types_de_champ.first.update!(type_champ: TypeDeChamp.type_champs.fetch(:piece_justificative))
|
||||||
|
draft_revision.reload
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'returns an up-to-date estimate' do
|
it 'returns an up-to-date estimate' do
|
||||||
|
@ -729,6 +754,7 @@ describe ProcedureRevision do
|
||||||
TypesDeChamp::TypeDeChampBase::FILL_DURATION_LONG \
|
TypesDeChamp::TypeDeChampBase::FILL_DURATION_LONG \
|
||||||
+ TypesDeChamp::TypeDeChampBase::FILL_DURATION_MEDIUM \
|
+ TypesDeChamp::TypeDeChampBase::FILL_DURATION_MEDIUM \
|
||||||
+ TypesDeChamp::TypeDeChampBase::FILL_DURATION_LONG \
|
+ TypesDeChamp::TypeDeChampBase::FILL_DURATION_LONG \
|
||||||
|
+ 3 * description_read_time
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue