commit
bbad67d271
24 changed files with 280 additions and 137 deletions
|
@ -91,7 +91,7 @@ module Experts
|
|||
redirect_to url_for(expert_all_avis_path)
|
||||
else
|
||||
flash[:alert] = user.errors.full_messages
|
||||
redirect_to url_for(sign_up_expert_avis_path(procedure_id, avis_id, email))
|
||||
redirect_to sign_up_expert_avis_path(procedure_id, avis_id, email: email)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -210,6 +210,7 @@ module Instructeurs
|
|||
@dossiers_funnel = @procedure.stats_dossiers_funnel
|
||||
@termines_states = @procedure.stats_termines_states
|
||||
@termines_by_week = @procedure.stats_termines_by_week
|
||||
@usual_traitement_time_by_month = @procedure.stats_usual_traitement_time_by_month_in_days
|
||||
end
|
||||
|
||||
private
|
||||
|
|
|
@ -5,6 +5,7 @@ module Users
|
|||
return procedure_not_found if @procedure.blank? || @procedure.brouillon?
|
||||
|
||||
@usual_traitement_time = @procedure.stats_usual_traitement_time
|
||||
@usual_traitement_time_by_month = @procedure.stats_usual_traitement_time_by_month_in_days
|
||||
@dossiers_funnel = @procedure.stats_dossiers_funnel
|
||||
@termines_states = @procedure.stats_termines_states
|
||||
@termines_by_week = @procedure.stats_termines_by_week
|
||||
|
|
|
@ -1,9 +1,21 @@
|
|||
module ProcedureStatsConcern
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
NB_DAYS_RECENT_DOSSIERS = 30
|
||||
# Percentage of dossiers considered to compute the 'usual traitement time'.
|
||||
# For instance, a value of '90' means that the usual traitement time will return
|
||||
# the duration under which 90% of the given dossiers are closed.
|
||||
USUAL_TRAITEMENT_TIME_PERCENTILE = 90
|
||||
|
||||
def stats_usual_traitement_time
|
||||
Rails.cache.fetch("#{cache_key_with_version}/stats_usual_traitement_time", expires_in: 12.hours) do
|
||||
usual_traitement_time
|
||||
usual_traitement_time_for_recent_dossiers(NB_DAYS_RECENT_DOSSIERS)
|
||||
end
|
||||
end
|
||||
|
||||
def stats_usual_traitement_time_by_month_in_days
|
||||
Rails.cache.fetch("#{cache_key_with_version}/stats_usual_traitement_time_by_month_in_days", expires_in: 12.hours) do
|
||||
usual_traitement_time_by_month_in_days
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -45,4 +57,51 @@ module ProcedureStatsConcern
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
def traitement_times(date_range)
|
||||
Traitement.for_traitement_time_stats(self)
|
||||
.where(processed_at: date_range)
|
||||
.pluck('dossiers.en_construction_at', :processed_at)
|
||||
.map { |en_construction_at, processed_at| { en_construction_at: en_construction_at, processed_at: processed_at } }
|
||||
end
|
||||
|
||||
def usual_traitement_time_by_month_in_days
|
||||
traitement_times(first_processed_at..last_considered_processed_at)
|
||||
.group_by { |t| t[:processed_at].beginning_of_month }
|
||||
.transform_values { |month| month.map { |h| h[:processed_at] - h[:en_construction_at] } }
|
||||
.transform_values { |traitement_times_for_month| traitement_times_for_month.percentile(USUAL_TRAITEMENT_TIME_PERCENTILE).ceil }
|
||||
.transform_values { |seconds| seconds == 0 ? nil : seconds }
|
||||
.transform_values { |seconds| convert_seconds_in_days(seconds) }
|
||||
.transform_keys { |month| pretty_month(month) }
|
||||
end
|
||||
|
||||
def usual_traitement_time_for_recent_dossiers(nb_days)
|
||||
now = Time.zone.now
|
||||
traitement_time =
|
||||
traitement_times((now - nb_days.days)..now)
|
||||
.map { |times| times[:processed_at] - times[:en_construction_at] }
|
||||
.percentile(USUAL_TRAITEMENT_TIME_PERCENTILE)
|
||||
.ceil
|
||||
|
||||
traitement_time = nil if traitement_time == 0
|
||||
traitement_time
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def first_processed_at
|
||||
Traitement.for_traitement_time_stats(self).pick(:processed_at)
|
||||
end
|
||||
|
||||
def last_considered_processed_at
|
||||
(Time.zone.now - 1.month).end_of_month
|
||||
end
|
||||
|
||||
def convert_seconds_in_days(seconds)
|
||||
(seconds / 60.0 / 60.0 / 24.0).ceil
|
||||
end
|
||||
|
||||
def pretty_month(month)
|
||||
I18n.l(month, format: "%B %Y")
|
||||
end
|
||||
end
|
||||
|
|
|
@ -565,19 +565,6 @@ class Procedure < ApplicationRecord
|
|||
end
|
||||
end
|
||||
|
||||
def usual_traitement_time
|
||||
times = Traitement.includes(:dossier)
|
||||
.where(dossier: self.dossiers)
|
||||
.where.not('dossiers.en_construction_at' => nil, :processed_at => nil)
|
||||
.where(processed_at: 1.month.ago..Time.zone.now)
|
||||
.pluck('dossiers.en_construction_at', :processed_at)
|
||||
.map { |(en_construction_at, processed_at)| processed_at - en_construction_at }
|
||||
|
||||
if times.present?
|
||||
times.percentile(90).ceil
|
||||
end
|
||||
end
|
||||
|
||||
def populate_champ_stable_ids
|
||||
TypeDeChamp
|
||||
.joins(:revisions)
|
||||
|
|
|
@ -19,10 +19,17 @@ class Traitement < ApplicationRecord
|
|||
.where("traitements.processed_at + (procedures.duree_conservation_dossiers_dans_ds * INTERVAL '1 month') - INTERVAL :expires_in < :now", { now: Time.zone.now, expires_in: Dossier::INTERVAL_BEFORE_EXPIRATION })
|
||||
end
|
||||
|
||||
scope :for_traitement_time_stats, -> (procedure) do
|
||||
includes(:dossier)
|
||||
.where(dossier: procedure.dossiers)
|
||||
.where.not('dossiers.en_construction_at' => nil, :processed_at => nil)
|
||||
.order(:processed_at)
|
||||
end
|
||||
|
||||
def self.count_dossiers_termines_by_month(groupe_instructeurs)
|
||||
last_traitements_per_dossier = Traitement
|
||||
.select('max(traitements.processed_at) as processed_at')
|
||||
.where(dossier: Dossier.termine.where(groupe_instructeur: groupe_instructeurs))
|
||||
.where(dossier: Dossier.state_termine.where(groupe_instructeur: groupe_instructeurs))
|
||||
.group(:dossier_id)
|
||||
.to_sql
|
||||
|
||||
|
|
|
@ -56,7 +56,9 @@ class PiecesJustificativesService
|
|||
include_infos_administration: true,
|
||||
dossier: dossier
|
||||
})
|
||||
dossier.pdf_export_for_instructeur.attach(io: StringIO.open(pdf), filename: "export-#{dossier.id}.pdf", content_type: 'application/pdf')
|
||||
ActiveRecord::Base.no_touching do
|
||||
dossier.pdf_export_for_instructeur.attach(io: StringIO.open(pdf), filename: "export-#{dossier.id}.pdf", content_type: 'application/pdf')
|
||||
end
|
||||
dossier.pdf_export_for_instructeur
|
||||
end
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
- content_for(:title, 'Invitation à donner votre avis')
|
||||
- avis_link = @avis.expert.user.active?.present? ? expert_avis_url(@avis.procedure, @avis) : sign_up_expert_avis_url(@avis.procedure, @avis.id, @avis.expert.email)
|
||||
- avis_link = @avis.expert.user.active?.present? ? expert_avis_url(@avis.procedure, @avis) : sign_up_expert_avis_url(@avis.procedure, @avis.id, email: @avis.expert.email)
|
||||
|
||||
- content_for(:footer) do
|
||||
Merci de ne pas répondre à cet email. Donnez votre avis
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
%p.description= @dossier.procedure.libelle
|
||||
%p.dossier Dossier nº #{@dossier.id}
|
||||
.column
|
||||
= form_for(User.new, url: { controller: "experts/avis", action: :update_expert }, method: :post, html: { class: "form" }) do |f|
|
||||
= form_for(User.new, url: sign_up_expert_avis_path(email: @email), method: :post, html: { class: "form" }) do |f|
|
||||
%h1 Créez-vous un compte
|
||||
|
||||
= f.label :email, "Email"
|
||||
|
|
|
@ -1,4 +1,29 @@
|
|||
- if Dossier::EN_CONSTRUCTION_OU_INSTRUCTION.include?(state)
|
||||
- if Dossier::TERMINE.include?(state)
|
||||
.dropdown.user-dossier-actions
|
||||
%button.button.dropdown-button{ 'aria-expanded' => 'false', 'aria-controls' => 'actions-menu' }
|
||||
Actions
|
||||
#actions-menu.dropdown-content.fade-in-down
|
||||
%ul.dropdown-items
|
||||
- if archived
|
||||
%li
|
||||
= link_to unarchive_instructeur_dossier_path(procedure_id, dossier_id), method: :patch do
|
||||
%span.icon.unarchive
|
||||
.dropdown-description
|
||||
Désarchiver le dossier
|
||||
- else
|
||||
%li
|
||||
= link_to archive_instructeur_dossier_path(procedure_id, dossier_id), method: :patch do
|
||||
%span.icon.archive
|
||||
.dropdown-description
|
||||
Archiver le dossier
|
||||
|
||||
%li.danger
|
||||
= link_to supprimer_dossier_instructeur_dossier_path(procedure_id, dossier_id), method: :patch, data: { confirm: "Voulez vous vraiment supprimer le dossier #{dossier_id} ? Cette action est irréversible. \nNous vous suggérons de télécharger le dossier au format PDF au préalable." } do
|
||||
%span.icon.delete
|
||||
.dropdown-description
|
||||
Supprimer le dossier
|
||||
|
||||
- elsif Dossier::EN_CONSTRUCTION_OU_INSTRUCTION.include?(state)
|
||||
- if dossier_is_followed
|
||||
= link_to unfollow_instructeur_dossier_path(procedure_id, dossier_id), method: :patch, class: 'button' do
|
||||
%span.icon.unfollow>
|
||||
|
@ -7,13 +32,3 @@
|
|||
= link_to follow_instructeur_dossier_path(procedure_id, dossier_id), method: :patch, class: 'button' do
|
||||
%span.icon.follow>
|
||||
Suivre le dossier
|
||||
|
||||
- elsif Dossier::TERMINE.include?(state)
|
||||
- if archived
|
||||
= link_to unarchive_instructeur_dossier_path(procedure_id, dossier_id), method: :patch, class: 'button' do
|
||||
%span.icon.unarchive>
|
||||
Désarchiver le dossier
|
||||
- else
|
||||
= link_to archive_instructeur_dossier_path(procedure_id, dossier_id), method: :patch, class: 'button' do
|
||||
%span.icon.archive>
|
||||
Archiver le dossier
|
||||
|
|
|
@ -11,15 +11,23 @@
|
|||
%span.big-number-card-number
|
||||
= distance_of_time_in_words(@usual_traitement_time)
|
||||
%span.big-number-card-detail
|
||||
90% des demandes du mois dernier ont été traitées en moins de #{distance_of_time_in_words(@usual_traitement_time)}.
|
||||
#{ProcedureStatsConcern::USUAL_TRAITEMENT_TIME_PERCENTILE}% des demandes des #{ProcedureStatsConcern::NB_DAYS_RECENT_DOSSIERS} derniers jours ont été traitées en moins de #{distance_of_time_in_words(@usual_traitement_time)}.
|
||||
|
||||
.stat-cards
|
||||
.stat-card.stat-card-half.pull-left
|
||||
%span.stat-card-title TEMPS DE TRAITEMENT
|
||||
.chart-container
|
||||
.chart
|
||||
- colors = %w(#C3D9FF #0069CC #1C7EC9) # from _colors.scss
|
||||
= column_chart @usual_traitement_time_by_month, ytitle: "Nb Jours", legend: "bottom", label: "Temps de traitement entre le passage en instruction et la réponse (accepté, refusé, ou classé sans suite) pour 90% des dossiers"
|
||||
|
||||
.stat-card.stat-card-half.pull-left
|
||||
%span.stat-card-title AVANCÉE DES DOSSIERS
|
||||
.chart-container
|
||||
.chart
|
||||
= area_chart @dossiers_funnel
|
||||
|
||||
.stat-cards
|
||||
.stat-card.stat-card-half.pull-left
|
||||
%span.stat-card-title TAUX D’ACCEPTATION
|
||||
.chart-container
|
||||
|
@ -27,9 +35,8 @@
|
|||
- colors = %w(#C3D9FF #0069CC #1C7EC9) # from _colors.scss
|
||||
= pie_chart @termines_states, colors: colors
|
||||
|
||||
.stat-cards
|
||||
.stat-card.stat-card-half.pull-left
|
||||
%span.stat-card-title RÉPARTITION PAR SEMAINE
|
||||
.chart-container
|
||||
.chart
|
||||
= line_chart @termines_by_week, colors: ["#387EC3", "#AE2C2B", "#FAD859"]
|
||||
.stat-card.stat-card-half.pull-left
|
||||
%span.stat-card-title RÉPARTITION PAR SEMAINE
|
||||
.chart-container
|
||||
.chart
|
||||
= line_chart @termines_by_week, colors: ["#387EC3", "#AE2C2B", "#FAD859"]
|
||||
|
|
|
@ -4,8 +4,8 @@
|
|||
- show_time_means = procedure.id != procedure_id_for_which_we_hide_the_estimated_delay && procedure.path != procedure_path_for_which_we_hide_the_estimated_delay
|
||||
|
||||
- cache(procedure.id, expires_in: 1.day) do
|
||||
- if procedure.usual_traitement_time && show_time_means
|
||||
- if procedure.usual_traitement_time_for_recent_dossiers(ProcedureStatsConcern::NB_DAYS_RECENT_DOSSIERS) && show_time_means
|
||||
%p
|
||||
Habituellement, les dossiers de cette démarche sont traités dans un délai de #{distance_of_time_in_words(procedure.usual_traitement_time)}.
|
||||
Habituellement, les dossiers de cette démarche sont traités dans un délai de #{distance_of_time_in_words(procedure.usual_traitement_time_for_recent_dossiers(ProcedureStatsConcern::NB_DAYS_RECENT_DOSSIERS))}.
|
||||
%p
|
||||
Cette estimation est calculée automatiquement à partir des délais d’instruction constatés précédemment. Le délai réel peut être différent, en fonction du type de démarche (par exemple pour un appel à projet avec date de décision fixe).
|
||||
Cette estimation est calculée automatiquement à partir des délais d’instruction constatés sur #{ProcedureStatsConcern::USUAL_TRAITEMENT_TIME_PERCENTILE}% des demandes qui ont été traitées lors des #{ProcedureStatsConcern::NB_DAYS_RECENT_DOSSIERS} derniers jours. Le délai réel peut être différent, en fonction du type de démarche (par exemple pour un appel à projet avec date de décision fixe).
|
||||
|
|
|
@ -105,7 +105,8 @@ Rails.application.routes.draw do
|
|||
devise_scope :user do
|
||||
get '/users/no_procedure' => 'users/sessions#no_procedure'
|
||||
get 'connexion-par-jeton/:id' => 'users/sessions#sign_in_by_link', as: 'sign_in_by_link'
|
||||
get 'lien-envoye/:email' => 'users/sessions#link_sent', constraints: { email: /.*/ }, as: 'link_sent'
|
||||
get 'lien-envoye' => 'users/sessions#link_sent', as: 'link_sent'
|
||||
get 'lien-envoye/:email' => 'users/sessions#link_sent', constraints: { email: /.*/ }, as: 'link_sent_legacy' # legacy, can be removed as soon as the previous line is deployed to production servers
|
||||
get '/users/password/reset-link-sent' => 'users/passwords#reset_link_sent'
|
||||
end
|
||||
|
||||
|
@ -300,10 +301,6 @@ Rails.application.routes.draw do
|
|||
#
|
||||
scope module: 'experts', as: 'expert' do
|
||||
get 'avis', to: 'avis#index', as: 'all_avis'
|
||||
# this redirections are ephemeral, to ensure that emails sent to experts before are still valid
|
||||
# TODO : they will be removed in September, 2020
|
||||
get 'avis/:id', to: redirect('/procedures/old/avis/%{id}')
|
||||
get 'avis/:id/sign_up/email/:email', to: redirect("/procedures/old/avis/%{id}/sign_up/email/%{email}"), constraints: { email: /.*/ }
|
||||
|
||||
resources :procedures, only: [], param: :procedure_id do
|
||||
member do
|
||||
|
@ -316,8 +313,13 @@ Rails.application.routes.draw do
|
|||
post 'avis' => 'avis#create_avis'
|
||||
get 'bilans_bdf'
|
||||
|
||||
get 'sign_up/email/:email' => 'avis#sign_up', constraints: { email: /.*/ }, as: 'sign_up'
|
||||
post 'sign_up/email/:email' => 'avis#update_expert', constraints: { email: /.*/ }
|
||||
get 'sign_up' => 'avis#sign_up'
|
||||
post 'sign_up' => 'avis#update_expert'
|
||||
|
||||
# This redirections are ephemeral, to ensure that emails sent to experts before are still valid
|
||||
# TODO : remove these lines after September, 2021
|
||||
get 'sign_up/email/:email' => 'avis#sign_up', constraints: { email: /.*/ }, as: 'sign_up_legacy'
|
||||
post 'sign_up/email/:email' => 'avis#update_expert', constraints: { email: /.*/ }, as: 'update_expert_legacy'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -329,11 +331,6 @@ Rails.application.routes.draw do
|
|||
#
|
||||
|
||||
scope module: 'instructeurs', as: 'instructeur' do
|
||||
# this redirections are ephemeral, to ensure that emails sent to experts before are still valid
|
||||
# TODO : they will be removed in September, 2020
|
||||
get 'avis/:id', to: redirect('/procedures/old/avis/%{id}')
|
||||
get 'avis/:id/sign_up/email/:email', to: redirect("/procedures/old/avis/%{id}/sign_up/email/%{email}"), constraints: { email: /.*/ }
|
||||
|
||||
resources :procedures, only: [:index, :show], param: :procedure_id do
|
||||
member do
|
||||
resources :groupes, only: [:index, :show], controller: 'groupe_instructeurs' do
|
||||
|
|
|
@ -17,7 +17,7 @@ class Array
|
|||
values = self.sort
|
||||
|
||||
if values.empty?
|
||||
return []
|
||||
return 0
|
||||
elsif values.size == 1
|
||||
return values.first
|
||||
elsif p == 100
|
||||
|
|
|
@ -12,7 +12,7 @@ feature 'Inviting an expert:' do
|
|||
|
||||
context 'when I don’t already have an account' do
|
||||
scenario 'I can sign up' do
|
||||
visit sign_up_expert_avis_path(avis.dossier.procedure, avis, avis.expert.email)
|
||||
visit sign_up_expert_avis_path(avis.dossier.procedure, avis, email: avis.expert.email)
|
||||
|
||||
expect(page).to have_field('Email', with: avis.expert.email, disabled: true)
|
||||
fill_in 'Mot de passe', with: 'This is a very complicated password !'
|
||||
|
@ -29,7 +29,7 @@ feature 'Inviting an expert:' do
|
|||
avis.expert.user.reload
|
||||
end
|
||||
scenario 'I can sign in' do
|
||||
visit sign_up_expert_avis_path(avis.dossier.procedure, avis, avis.expert.email)
|
||||
visit sign_up_expert_avis_path(avis.dossier.procedure, avis, email: avis.expert.email)
|
||||
|
||||
expect(page).to have_current_path(new_user_session_path)
|
||||
login_as avis.expert.user, scope: :user
|
||||
|
|
|
@ -45,7 +45,7 @@ feature 'Inviting an expert:', js: true do
|
|||
|
||||
invitation_email = open_email(expert.email.to_s)
|
||||
avis = expert.avis.find_by(dossier: dossier)
|
||||
sign_up_link = sign_up_expert_avis_path(avis.dossier.procedure, avis, avis.expert.email)
|
||||
sign_up_link = sign_up_expert_avis_path(avis.dossier.procedure, avis, email: avis.expert.email)
|
||||
expect(invitation_email.body).to include(sign_up_link)
|
||||
end
|
||||
|
||||
|
|
|
@ -66,6 +66,15 @@ feature 'Instructing a dossier:', js: true do
|
|||
dossier.reload
|
||||
expect(dossier.state).to eq(Dossier.states.fetch(:accepte))
|
||||
expect(dossier.motivation).to eq('a good reason')
|
||||
|
||||
click_on procedure.libelle
|
||||
click_on 'traité'
|
||||
click_on 'Actions'
|
||||
accept_confirm do
|
||||
click_on 'Supprimer le dossier'
|
||||
end
|
||||
click_on 'traité'
|
||||
expect(page).not_to have_button('Actions')
|
||||
end
|
||||
|
||||
scenario 'A instructeur can follow/unfollow a dossier' do
|
||||
|
|
|
@ -14,7 +14,7 @@ RSpec.describe AvisMailer, type: :mailer do
|
|||
it { expect(subject.body).to include(instructeur_avis_url(avis.dossier.procedure.id, avis)) }
|
||||
|
||||
context 'when the recipient is not already registered' do
|
||||
it { expect(subject.body).to include(sign_up_expert_avis_url(avis.dossier.procedure.id, avis.id, avis.expert.email)) }
|
||||
it { expect(subject.body).to include(sign_up_expert_avis_url(avis.dossier.procedure.id, avis.id, email: avis.expert.email)) }
|
||||
end
|
||||
|
||||
context 'when the dossier has been deleted before the avis was sent' do
|
||||
|
|
94
spec/models/concern/procedure_stats_concern_spec.rb
Normal file
94
spec/models/concern/procedure_stats_concern_spec.rb
Normal file
|
@ -0,0 +1,94 @@
|
|||
describe ProcedureStatsConcern do
|
||||
describe '#usual_traitement_time_for_recent_dossiers' do
|
||||
let(:procedure) { create(:procedure) }
|
||||
|
||||
before do
|
||||
Timecop.freeze(Time.utc(2019, 6, 1, 12, 0))
|
||||
|
||||
delays.each do |delay|
|
||||
create_dossier(construction_date: 1.week.ago - delay, instruction_date: 1.week.ago - delay + 12.hours, processed_date: 1.week.ago)
|
||||
end
|
||||
end
|
||||
|
||||
after { Timecop.return }
|
||||
|
||||
context 'when there are several processed dossiers' do
|
||||
let(:delays) { [1.day, 2.days, 2.days, 2.days, 2.days, 3.days, 3.days, 3.days, 3.days, 12.days] }
|
||||
|
||||
it 'returns a time representative of the dossier instruction delay' do
|
||||
expect(procedure.usual_traitement_time_for_recent_dossiers(30)).to be_between(3.days, 4.days)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when there are very old dossiers' do
|
||||
let(:delays) { [2.days, 2.days] }
|
||||
let!(:old_dossier) { create_dossier(construction_date: 3.months.ago, instruction_date: 2.months.ago, processed_date: 2.months.ago) }
|
||||
|
||||
it 'ignores dossiers older than 1 month' do
|
||||
expect(procedure.usual_traitement_time_for_recent_dossiers(30)).to be_within(1.hour).of(2.days)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when there is a dossier with bad data' do
|
||||
let(:delays) { [2.days, 2.days] }
|
||||
let!(:bad_dossier) { create_dossier(construction_date: nil, instruction_date: nil, processed_date: 10.days.ago) }
|
||||
|
||||
it 'ignores bad dossiers' do
|
||||
expect(procedure.usual_traitement_time_for_recent_dossiers(30)).to be_within(1.hour).of(2.days)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when there is only one processed dossier' do
|
||||
let(:delays) { [1.day] }
|
||||
it { expect(procedure.usual_traitement_time_for_recent_dossiers(30)).to be_within(1.hour).of(1.day) }
|
||||
end
|
||||
|
||||
context 'where there is no processed dossier' do
|
||||
let(:delays) { [] }
|
||||
it { expect(procedure.usual_traitement_time_for_recent_dossiers(30)).to eq nil }
|
||||
end
|
||||
end
|
||||
|
||||
describe '.usual_traitement_time_by_month_in_days' do
|
||||
let(:procedure) { create(:procedure) }
|
||||
|
||||
def create_dossiers(delays_by_month)
|
||||
delays_by_month.each_with_index do |delays, index|
|
||||
delays.each do |delay|
|
||||
processed_date = (index.months + 1.week).ago
|
||||
create_dossier(construction_date: processed_date - delay, instruction_date: processed_date - delay + 12.hours, processed_date: processed_date)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
before do
|
||||
Timecop.freeze(Time.utc(2019, 6, 25, 12, 0))
|
||||
|
||||
create_dossiers(delays_by_month)
|
||||
end
|
||||
|
||||
after { Timecop.return }
|
||||
|
||||
context 'when there are several processed dossiers' do
|
||||
let(:delays_by_month) {
|
||||
[
|
||||
[90.days, 90.days],
|
||||
[1.day, 2.days, 2.days, 2.days, 2.days, 3.days, 3.days, 3.days, 3.days, 12.days],
|
||||
[30.days, 60.days, 60.days, 60.days]
|
||||
]
|
||||
}
|
||||
|
||||
it 'computes a time representative of the dossier instruction delay for each month except current month' do
|
||||
expect(procedure.usual_traitement_time_by_month_in_days['avril 2019']).to eq 60
|
||||
expect(procedure.usual_traitement_time_by_month_in_days['mai 2019']).to eq 4
|
||||
expect(procedure.usual_traitement_time_by_month_in_days['juin 2019']).to eq nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def create_dossier(construction_date:, instruction_date:, processed_date:)
|
||||
dossier = create(:dossier, :accepte, procedure: procedure, en_construction_at: construction_date, en_instruction_at: instruction_date, processed_at: processed_date)
|
||||
end
|
||||
end
|
|
@ -980,60 +980,6 @@ describe Procedure do
|
|||
end
|
||||
end
|
||||
|
||||
describe '#usual_traitement_time' do
|
||||
let(:procedure) { create(:procedure) }
|
||||
|
||||
def create_dossier(construction_date:, instruction_date:, processed_date:)
|
||||
dossier = create(:dossier, :accepte, procedure: procedure, en_construction_at: construction_date, en_instruction_at: instruction_date, processed_at: processed_date)
|
||||
end
|
||||
|
||||
before do
|
||||
Timecop.freeze(Time.utc(2019, 6, 1, 12, 0))
|
||||
|
||||
delays.each do |delay|
|
||||
create_dossier(construction_date: 1.week.ago - delay, instruction_date: 1.week.ago - delay + 12.hours, processed_date: 1.week.ago)
|
||||
end
|
||||
end
|
||||
|
||||
after { Timecop.return }
|
||||
|
||||
context 'when there are several processed dossiers' do
|
||||
let(:delays) { [1.day, 2.days, 2.days, 2.days, 2.days, 3.days, 3.days, 3.days, 3.days, 12.days] }
|
||||
|
||||
it 'returns a time representative of the dossier instruction delay' do
|
||||
expect(procedure.usual_traitement_time).to be_between(3.days, 4.days)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when there are very old dossiers' do
|
||||
let(:delays) { [2.days, 2.days] }
|
||||
let!(:old_dossier) { create_dossier(construction_date: 3.months.ago, instruction_date: 2.months.ago, processed_date: 2.months.ago) }
|
||||
|
||||
it 'ignores dossiers older than 1 month' do
|
||||
expect(procedure.usual_traitement_time).to be_within(1.hour).of(2.days)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when there is a dossier with bad data' do
|
||||
let(:delays) { [2.days, 2.days] }
|
||||
let!(:bad_dossier) { create_dossier(construction_date: nil, instruction_date: nil, processed_date: 10.days.ago) }
|
||||
|
||||
it 'ignores bad dossiers' do
|
||||
expect(procedure.usual_traitement_time).to be_within(1.hour).of(2.days)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when there is only one processed dossier' do
|
||||
let(:delays) { [1.day] }
|
||||
it { expect(procedure.usual_traitement_time).to be_within(1.hour).of(1.day) }
|
||||
end
|
||||
|
||||
context 'where there is no processed dossier' do
|
||||
let(:delays) { [] }
|
||||
it { expect(procedure.usual_traitement_time).to be_nil }
|
||||
end
|
||||
end
|
||||
|
||||
describe '.ensure_a_groupe_instructeur_exists' do
|
||||
let!(:procedure) { create(:procedure) }
|
||||
|
||||
|
@ -1082,12 +1028,12 @@ describe Procedure do
|
|||
it 'estimates average dossier weight' do
|
||||
expect(procedure.reload.average_dossier_weight).to eq 5
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
private
|
||||
|
||||
def create_dossier_with_pj_of_size(size, procedure)
|
||||
dossier = create(:dossier, :accepte, procedure: procedure)
|
||||
create(:champ_piece_justificative, size: size, dossier: dossier)
|
||||
end
|
||||
def create_dossier_with_pj_of_size(size, procedure)
|
||||
dossier = create(:dossier, :accepte, procedure: procedure)
|
||||
create(:champ_piece_justificative, size: size, dossier: dossier)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -2,23 +2,28 @@ describe Traitement do
|
|||
describe '#count_dossiers_termines_by_month' do
|
||||
let(:procedure) { create(:procedure, :published, groupe_instructeurs: [groupe_instructeurs]) }
|
||||
let(:groupe_instructeurs) { create(:groupe_instructeur) }
|
||||
let(:result) { Traitement.count_dossiers_termines_by_month(groupe_instructeurs) }
|
||||
|
||||
before do
|
||||
create_dossier_for_month(procedure, 2021, 3)
|
||||
create_dossier_for_month(procedure, 2021, 3)
|
||||
create_archived_dossier_for_month(procedure, 2021, 3)
|
||||
create_dossier_for_month(procedure, 2021, 2)
|
||||
Timecop.freeze(Time.zone.local(2021, 3, 5))
|
||||
end
|
||||
|
||||
subject do
|
||||
Timecop.freeze(Time.zone.local(2021, 3, 5)) do
|
||||
Traitement.count_dossiers_termines_by_month(groupe_instructeurs)
|
||||
end
|
||||
end
|
||||
|
||||
it 'count dossiers_termines by month' do
|
||||
expect(count_for_month(result, 3)).to eq 2
|
||||
expect(count_for_month(result, 2)).to eq 1
|
||||
expect(count_for_month(subject, 3)).to eq 3
|
||||
expect(count_for_month(subject, 2)).to eq 1
|
||||
end
|
||||
|
||||
it 'returns descending order by month' do
|
||||
expect(result[0]["month"].to_date.month).to eq 3
|
||||
expect(result[1]["month"].to_date.month).to eq 2
|
||||
expect(subject[0]["month"].to_date.month).to eq 3
|
||||
expect(subject[1]["month"].to_date.month).to eq 2
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -31,7 +36,14 @@ describe Traitement do
|
|||
end
|
||||
|
||||
def create_dossier_for_month(procedure, year, month)
|
||||
Timecop.freeze(Time.zone.local(year, month, 5))
|
||||
create(:dossier, :accepte, :with_attestation, procedure: procedure)
|
||||
Timecop.freeze(Time.zone.local(year, month, 5)) do
|
||||
create(:dossier, :accepte, :with_attestation, procedure: procedure)
|
||||
end
|
||||
end
|
||||
|
||||
def create_archived_dossier_for_month(procedure, year, month)
|
||||
Timecop.freeze(Time.zone.local(year, month, 5)) do
|
||||
create(:dossier, :accepte, :archived, :with_attestation, procedure: procedure)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -44,4 +44,18 @@ describe PiecesJustificativesService do
|
|||
expect(subject.any? { |piece| piece.name == 'serialized' }).to be_truthy
|
||||
end
|
||||
end
|
||||
|
||||
describe '.generate_dossier_export' do
|
||||
subject { PiecesJustificativesService.generate_dossier_export(dossier) }
|
||||
it "generates pdf export for instructeur" do
|
||||
subject
|
||||
expect(dossier.pdf_export_for_instructeur).to be_attached
|
||||
end
|
||||
|
||||
it "doesn't update dossier" do
|
||||
before_export = Time.zone.now
|
||||
subject
|
||||
expect(dossier.updated_at).to be <= before_export
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
describe 'users/dossiers/show/_status_overview.html.haml', type: :view do
|
||||
before { allow(dossier.procedure).to receive(:usual_traitement_time).and_return(1.day) }
|
||||
before { allow(dossier.procedure).to receive(:usual_traitement_time_for_recent_dossiers).and_return(1.day) }
|
||||
|
||||
subject! { render 'users/dossiers/show/status_overview.html.haml', dossier: dossier }
|
||||
|
||||
|
|
16
yarn.lock
16
yarn.lock
|
@ -3248,15 +3248,7 @@ chalk@^3.0.0, chalk@^3.0.0-beta.2:
|
|||
ansi-styles "^4.1.0"
|
||||
supports-color "^7.1.0"
|
||||
|
||||
chalk@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.0.0.tgz#6e98081ed2d17faab615eb52ac66ec1fe6209e72"
|
||||
integrity sha512-N9oWFcegS0sFr9oh1oz2d7Npos6vNoWW9HvtCg5N1KRFpUhaAhvTv5Y58g880fZaEYSNm3qDz8SU1UrGvp+n7A==
|
||||
dependencies:
|
||||
ansi-styles "^4.1.0"
|
||||
supports-color "^7.1.0"
|
||||
|
||||
chalk@^4.1.0:
|
||||
chalk@^4.0.0, chalk@^4.1.0:
|
||||
version "4.1.0"
|
||||
resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.0.tgz#4e14870a618d9e2edd97dd8345fd9d9dc315646a"
|
||||
integrity sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==
|
||||
|
@ -10090,9 +10082,9 @@ postcss-values-parser@^2.0.0, postcss-values-parser@^2.0.1:
|
|||
uniq "^1.0.1"
|
||||
|
||||
postcss@^7.0.0, postcss@^7.0.1, postcss@^7.0.14, postcss@^7.0.16, postcss@^7.0.17, postcss@^7.0.2, postcss@^7.0.26, postcss@^7.0.27, postcss@^7.0.5, postcss@^7.0.6:
|
||||
version "7.0.30"
|
||||
resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.30.tgz#cc9378beffe46a02cbc4506a0477d05fcea9a8e2"
|
||||
integrity sha512-nu/0m+NtIzoubO+xdAlwZl/u5S5vi/y6BCsoL8D+8IxsD3XvBS8X4YEADNIVXKVuQvduiucnRv+vPIqj56EGMQ==
|
||||
version "7.0.36"
|
||||
resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.36.tgz#056f8cffa939662a8f5905950c07d5285644dfcb"
|
||||
integrity sha512-BebJSIUMwJHRH0HAQoxN4u1CN86glsrwsW0q7T+/m44eXOUAxSNdHRkNZPYz5vVUbg17hFgOQDE7fZk7li3pZw==
|
||||
dependencies:
|
||||
chalk "^2.4.2"
|
||||
source-map "^0.6.1"
|
||||
|
|
Loading…
Add table
Reference in a new issue