diff --git a/app/controllers/experts/avis_controller.rb b/app/controllers/experts/avis_controller.rb index 78de84947..04114174a 100644 --- a/app/controllers/experts/avis_controller.rb +++ b/app/controllers/experts/avis_controller.rb @@ -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 diff --git a/app/controllers/instructeurs/procedures_controller.rb b/app/controllers/instructeurs/procedures_controller.rb index b96c83dc5..bb556675c 100644 --- a/app/controllers/instructeurs/procedures_controller.rb +++ b/app/controllers/instructeurs/procedures_controller.rb @@ -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 diff --git a/app/controllers/users/statistiques_controller.rb b/app/controllers/users/statistiques_controller.rb index 8b1611027..5ccafc2bf 100644 --- a/app/controllers/users/statistiques_controller.rb +++ b/app/controllers/users/statistiques_controller.rb @@ -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 diff --git a/app/models/concerns/procedure_stats_concern.rb b/app/models/concerns/procedure_stats_concern.rb index 6c8414e8c..cbe89c5a8 100644 --- a/app/models/concerns/procedure_stats_concern.rb +++ b/app/models/concerns/procedure_stats_concern.rb @@ -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 diff --git a/app/models/procedure.rb b/app/models/procedure.rb index 0af87e5a9..07ca5e6ff 100644 --- a/app/models/procedure.rb +++ b/app/models/procedure.rb @@ -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) diff --git a/app/models/traitement.rb b/app/models/traitement.rb index 8bb604945..c382b9e79 100644 --- a/app/models/traitement.rb +++ b/app/models/traitement.rb @@ -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 diff --git a/app/services/pieces_justificatives_service.rb b/app/services/pieces_justificatives_service.rb index 1d337f52c..08b358d4c 100644 --- a/app/services/pieces_justificatives_service.rb +++ b/app/services/pieces_justificatives_service.rb @@ -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 diff --git a/app/views/avis_mailer/avis_invitation.html.haml b/app/views/avis_mailer/avis_invitation.html.haml index 52f08054d..319d0aa39 100644 --- a/app/views/avis_mailer/avis_invitation.html.haml +++ b/app/views/avis_mailer/avis_invitation.html.haml @@ -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 diff --git a/app/views/experts/avis/sign_up.html.haml b/app/views/experts/avis/sign_up.html.haml index f93ed115f..3c4724d0b 100644 --- a/app/views/experts/avis/sign_up.html.haml +++ b/app/views/experts/avis/sign_up.html.haml @@ -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" diff --git a/app/views/instructeurs/procedures/_dossier_actions.html.haml b/app/views/instructeurs/procedures/_dossier_actions.html.haml index 14983ffa8..710330bcb 100644 --- a/app/views/instructeurs/procedures/_dossier_actions.html.haml +++ b/app/views/instructeurs/procedures/_dossier_actions.html.haml @@ -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 diff --git a/app/views/shared/procedures/_stats.html.haml b/app/views/shared/procedures/_stats.html.haml index fb6925914..2da3e10bc 100644 --- a/app/views/shared/procedures/_stats.html.haml +++ b/app/views/shared/procedures/_stats.html.haml @@ -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"] diff --git a/app/views/users/dossiers/show/_estimated_delay.html.haml b/app/views/users/dossiers/show/_estimated_delay.html.haml index eabc51bb3..e33eab09c 100644 --- a/app/views/users/dossiers/show/_estimated_delay.html.haml +++ b/app/views/users/dossiers/show/_estimated_delay.html.haml @@ -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). diff --git a/config/routes.rb b/config/routes.rb index 9dba2d095..22a203093 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -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 diff --git a/lib/core_ext/array.rb b/lib/core_ext/array.rb index 00aa9f59c..39d7ad777 100644 --- a/lib/core_ext/array.rb +++ b/lib/core_ext/array.rb @@ -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 diff --git a/spec/features/experts/expert_spec.rb b/spec/features/experts/expert_spec.rb index 6c2f45c05..c2aea0a66 100644 --- a/spec/features/experts/expert_spec.rb +++ b/spec/features/experts/expert_spec.rb @@ -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 diff --git a/spec/features/instructeurs/expert_spec.rb b/spec/features/instructeurs/expert_spec.rb index 20abe6017..84b3d56a6 100644 --- a/spec/features/instructeurs/expert_spec.rb +++ b/spec/features/instructeurs/expert_spec.rb @@ -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 diff --git a/spec/features/instructeurs/instruction_spec.rb b/spec/features/instructeurs/instruction_spec.rb index 1b1bca949..190f8903d 100644 --- a/spec/features/instructeurs/instruction_spec.rb +++ b/spec/features/instructeurs/instruction_spec.rb @@ -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 diff --git a/spec/mailers/avis_mailer_spec.rb b/spec/mailers/avis_mailer_spec.rb index 6eac2062f..d9d822d5f 100644 --- a/spec/mailers/avis_mailer_spec.rb +++ b/spec/mailers/avis_mailer_spec.rb @@ -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 diff --git a/spec/models/concern/procedure_stats_concern_spec.rb b/spec/models/concern/procedure_stats_concern_spec.rb new file mode 100644 index 000000000..050989259 --- /dev/null +++ b/spec/models/concern/procedure_stats_concern_spec.rb @@ -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 diff --git a/spec/models/procedure_spec.rb b/spec/models/procedure_spec.rb index 1f7327fdb..9850686d4 100644 --- a/spec/models/procedure_spec.rb +++ b/spec/models/procedure_spec.rb @@ -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 diff --git a/spec/models/traitement_spec.rb b/spec/models/traitement_spec.rb index 42465e8b1..d84bdeedc 100644 --- a/spec/models/traitement_spec.rb +++ b/spec/models/traitement_spec.rb @@ -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 diff --git a/spec/services/pieces_justificatives_service_spec.rb b/spec/services/pieces_justificatives_service_spec.rb index b4e7b01a9..652ea9819 100644 --- a/spec/services/pieces_justificatives_service_spec.rb +++ b/spec/services/pieces_justificatives_service_spec.rb @@ -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 diff --git a/spec/views/users/dossiers/show/_status_overview.html.haml_spec.rb b/spec/views/users/dossiers/show/_status_overview.html.haml_spec.rb index 71362ea7d..5f95bd3b7 100644 --- a/spec/views/users/dossiers/show/_status_overview.html.haml_spec.rb +++ b/spec/views/users/dossiers/show/_status_overview.html.haml_spec.rb @@ -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 } diff --git a/yarn.lock b/yarn.lock index e67528021..93c0ca786 100644 --- a/yarn.lock +++ b/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"