Merge pull request #6718 from betagouv/main

This commit is contained in:
Pierre de La Morinerie 2021-12-02 16:16:32 +01:00 committed by GitHub
commit 426c682fcb
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
28 changed files with 180 additions and 504 deletions

View file

@ -109,6 +109,10 @@ Pour exécuter les tests de l'application, plusieurs possibilités :
bin/rspec --only-failures bin/rspec --only-failures
- Lancer un ou des tests systèmes avec un browser
NO_HEADLESS=1 bin/rspec spec/system
### Ajout de taches à exécuter au déploiement ### Ajout de taches à exécuter au déploiement
rails generate after_party:task task_name rails generate after_party:task task_name

View file

@ -224,7 +224,7 @@ class ApplicationController < ActionController::Base
def redirect_if_untrusted def redirect_if_untrusted
if instructeur_signed_in? && if instructeur_signed_in? &&
sensitive_path && sensitive_path &&
!feature_enabled?(:instructeur_bypass_email_login_token) && !current_instructeur.bypass_email_login_token &&
!IPService.ip_trusted?(request.headers['X-Forwarded-For']) && !IPService.ip_trusted?(request.headers['X-Forwarded-For']) &&
!trusted_device? !trusted_device?

View file

@ -1,23 +1,5 @@
module Manager module Manager
class InstructeursController < Manager::ApplicationController class InstructeursController < Manager::ApplicationController
# Temporary code: synchronize Flipper's instructeur_bypass_email_login_token
# when Instructeur.bypass_email_login_token is modified.
#
# This will be removed when the migration of this feature flag out of Flipper will be complete.
def update
super
instructeur = requested_resource
saved_successfully = !requested_resource.changed?
if saved_successfully
if instructeur.bypass_email_login_token
Flipper.enable_actor(:instructeur_bypass_email_login_token, instructeur.user)
else
Flipper.disable_actor(:instructeur_bypass_email_login_token, instructeur.user)
end
end
end
def reinvite def reinvite
instructeur = Instructeur.find(params[:id]) instructeur = Instructeur.find(params[:id])
instructeur.user.invite! instructeur.user.invite!

View file

@ -7,7 +7,6 @@ class StatsController < ApplicationController
stat = Stat.first stat = Stat.first
procedures = Procedure.publiees_ou_closes procedures = Procedure.publiees_ou_closes
dossiers = Dossier.state_not_brouillon
@procedures_numbers = procedures_numbers(procedures) @procedures_numbers = procedures_numbers(procedures)
@ -17,7 +16,9 @@ class StatsController < ApplicationController
stat.dossiers_deposes_entre_60_et_30_jours stat.dossiers_deposes_entre_60_et_30_jours
) )
@contact_percentage = contact_percentage @contact_percentage = Rails.cache.fetch("stats.contact_percentage", expires_in: 1.day) do
contact_percentage
end
@dossiers_states_for_pie = { @dossiers_states_for_pie = {
"Brouillon" => stat.dossiers_brouillon, "Brouillon" => stat.dossiers_brouillon,
@ -31,25 +32,6 @@ class StatsController < ApplicationController
@dossiers_cumulative = stat.dossiers_cumulative @dossiers_cumulative = stat.dossiers_cumulative
@dossiers_in_the_last_4_months = stat.dossiers_in_the_last_4_months @dossiers_in_the_last_4_months = stat.dossiers_in_the_last_4_months
if super_admin_signed_in?
@dossier_instruction_mean_time = Rails.cache.fetch("dossier_instruction_mean_time", expires_in: 1.day) do
dossier_instruction_mean_time(dossiers)
end
@dossier_filling_mean_time = Rails.cache.fetch("dossier_filling_mean_time", expires_in: 1.day) do
dossier_filling_mean_time(dossiers)
end
@avis_usage = avis_usage
@avis_average_answer_time = avis_average_answer_time
@avis_answer_percentages = avis_answer_percentages
@motivation_usage_dossier = motivation_usage_dossier
@motivation_usage_procedure = motivation_usage_procedure
@cloned_from_library_procedures_ratio = cloned_from_library_procedures_ratio
end
end end
def download def download
@ -146,22 +128,6 @@ class StatsController < ApplicationController
end end
end end
def cloned_from_library_procedures_ratio
[3.weeks.ago, 2.weeks.ago, 1.week.ago].map do |date|
min_date = date.beginning_of_week
max_date = min_date.end_of_week
all_procedures = Procedure.created_during(min_date..max_date)
cloned_from_library_procedures = all_procedures.cloned_from_library
denominator = [1, all_procedures.count].max
ratio = percentage(cloned_from_library_procedures.count, denominator)
[l(max_date, format: '%d/%m/%Y'), ratio]
end
end
def max_date def max_date
if super_admin_signed_in? if super_admin_signed_in?
Time.zone.now Time.zone.now
@ -193,220 +159,4 @@ class StatsController < ApplicationController
.map { |x, y| { x => (sum += y) } } .map { |x, y| { x => (sum += y) } }
.reduce({}, :merge) .reduce({}, :merge)
end end
def mean(collection)
(collection.sum.to_f / collection.size).round(2)
end
def percentage(numerator, denominator)
((numerator.to_f / denominator) * 100).round(2)
end
def dossier_instruction_mean_time(dossiers)
# In the 12 last months, we compute for each month
# the average time it took to instruct a dossier
# We compute monthly averages by first making an average per procedure
# and then computing the average for all the procedures
min_date = 11.months.ago
max_date = Time.zone.now.to_date
processed_dossiers = Traitement.includes(:dossier)
.where(dossier_id: dossiers)
.where('dossiers.state' => Dossier::TERMINE)
.where(:processed_at => min_date..max_date)
.pluck('dossiers.groupe_instructeur_id', 'dossiers.en_construction_at', :processed_at)
# Group dossiers by month
processed_dossiers_by_month = processed_dossiers
.group_by do |dossier|
dossier[2].beginning_of_month.to_s
end
processed_dossiers_by_month.map do |month, value|
# Group the dossiers for this month by procedure
dossiers_grouped_by_groupe_instructeur = value.group_by { |dossier| dossier[0] }
# Compute the mean time for this procedure
procedure_processing_times = dossiers_grouped_by_groupe_instructeur.map do |_procedure_id, procedure_dossiers|
procedure_dossiers_processing_time = procedure_dossiers.map do |dossier|
(dossier[2] - dossier[1]).to_f / (3600 * 24)
end
mean(procedure_dossiers_processing_time)
end
# Compute the average mean time for all the procedures of this month
month_average = mean(procedure_processing_times)
[month, month_average]
end.to_h
end
def dossier_filling_mean_time(dossiers)
# In the 12 last months, we compute for each month
# the average time it took to fill a dossier
# We compute monthly averages by first making an average per procedure
# and then computing the average for all the procedures
# For each procedure, we normalize the data: the time is calculated
# for a 24 champs form (the current form mean length)
min_date = 11.months.ago
max_date = Time.zone.now.to_date
processed_dossiers = Traitement.includes(:dossier)
.where(dossier: dossiers)
.where('dossiers.state' => Dossier::TERMINE)
.where(:processed_at => min_date..max_date)
.pluck(
'dossiers.groupe_instructeur_id',
Arel.sql('EXTRACT(EPOCH FROM (dossiers.en_construction_at - dossiers.created_at)) / 60 AS processing_time'),
:processed_at
)
# Group dossiers by month
processed_dossiers_by_month = processed_dossiers
.group_by do |(*_, processed_at)|
processed_at.beginning_of_month.to_s
end
groupe_instructeur_ids = processed_dossiers.map { |gid, _, _| gid }.uniq
groupe_instructeurs = GroupeInstructeur.where(id: groupe_instructeur_ids).pluck(:id, :procedure_id)
procedure_id_type_de_champs_count = TypeDeChamp
.where(private: false)
.joins(:revision)
.group('procedure_revisions.procedure_id')
.count
groupe_instructeur_id_type_de_champs_count = groupe_instructeurs.reduce({}) do |acc, (gi_id, procedure_id)|
acc[gi_id] = procedure_id_type_de_champs_count[procedure_id]
acc
end
processed_dossiers_by_month.map do |month, dossier_plucks|
# Group the dossiers for this month by procedure
dossiers_grouped_by_groupe_instructeur = dossier_plucks.group_by { |(groupe_instructeur_id, *_)| groupe_instructeur_id }
# Compute the mean time for this procedure
procedure_processing_times = dossiers_grouped_by_groupe_instructeur.filter_map do |groupe_instructeur_id, procedure_dossiers|
procedure_fields_count = groupe_instructeur_id_type_de_champs_count[groupe_instructeur_id]
if (procedure_fields_count == 0 || procedure_fields_count.nil?)
next
end
procedure_dossiers_processing_time = procedure_dossiers.map { |_, processing_time, _| processing_time }
procedure_mean = mean(procedure_dossiers_processing_time)
# We normalize the data for 24 fields
procedure_mean * (MEAN_NUMBER_OF_CHAMPS_IN_A_FORM / procedure_fields_count)
end
# Compute the average mean time for all the procedures of this month
month_average = mean(procedure_processing_times)
[month, month_average]
end.to_h
end
def avis_usage
[3.weeks.ago, 2.weeks.ago, 1.week.ago].map do |min_date|
max_date = min_date + 1.week
weekly_dossiers = Dossier.includes(:avis).where(created_at: min_date..max_date).to_a
weekly_dossiers_count = weekly_dossiers.count
if weekly_dossiers_count == 0
result = 0
else
weekly_dossier_with_avis_count = weekly_dossiers.count { |dossier| dossier.avis.present? }
result = percentage(weekly_dossier_with_avis_count, weekly_dossiers_count)
end
[min_date.to_i, result]
end
end
def avis_average_answer_time
[3.weeks.ago, 2.weeks.ago, 1.week.ago].map do |min_date|
max_date = min_date + 1.week
average = Avis.with_answer
.where(created_at: min_date..max_date)
.average("EXTRACT(EPOCH FROM avis.updated_at - avis.created_at) / 86400")
result = average ? average.to_f.round(2) : 0
[min_date.to_i, result]
end
end
def avis_answer_percentages
[3.weeks.ago, 2.weeks.ago, 1.week.ago].map do |min_date|
max_date = min_date + 1.week
weekly_avis = Avis.where(created_at: min_date..max_date)
weekly_avis_count = weekly_avis.count
if weekly_avis_count == 0
[min_date.to_i, 0]
else
answered_weekly_avis_count = weekly_avis.with_answer.count
result = percentage(answered_weekly_avis_count, weekly_avis_count)
[min_date.to_i, result]
end
end
end
def motivation_usage_dossier
[3.weeks.ago, 2.weeks.ago, 1.week.ago].map do |date|
min_date = date.beginning_of_week
max_date = date.end_of_week
weekly_termine_dossiers = Dossier.where(processed_at: min_date..max_date)
weekly_termine_dossiers_count = weekly_termine_dossiers.count
weekly_termine_dossiers_with_motivation_count = weekly_termine_dossiers.where.not(motivation: nil).count
if weekly_termine_dossiers_count == 0
result = 0
else
result = percentage(weekly_termine_dossiers_with_motivation_count, weekly_termine_dossiers_count)
end
[l(max_date, format: '%d/%m/%Y'), result]
end
end
def motivation_usage_procedure
[3.weeks.ago, 2.weeks.ago, 1.week.ago].map do |date|
min_date = date.beginning_of_week
max_date = date.end_of_week
procedures_with_dossier_processed_this_week = Procedure
.joins(:dossiers)
.where(dossiers: { processed_at: min_date..max_date })
procedures_with_dossier_processed_this_week_count = procedures_with_dossier_processed_this_week
.uniq
.count
procedures_with_dossier_processed_this_week_and_with_motivation_count = procedures_with_dossier_processed_this_week
.where
.not(dossiers: { motivation: nil })
.uniq
.count
if procedures_with_dossier_processed_this_week_count == 0
result = 0
else
result = percentage(procedures_with_dossier_processed_this_week_and_with_motivation_count, procedures_with_dossier_processed_this_week_count)
end
[l(max_date, format: '%d/%m/%Y'), result]
end
end
end end

View file

@ -110,7 +110,7 @@ function updateTypeDeChamp(
break; break;
case 'drop_down_list': case 'drop_down_list':
case 'multiple_drop_down_list': case 'multiple_drop_down_list':
typeDeChamp.drop_down_list_value = '--Premier élément du menu--\n'; typeDeChamp.drop_down_list_value = 'Premier choix\nDeuxième choix';
} }
} }

View file

@ -545,7 +545,7 @@ class Dossier < ApplicationRecord
end end
def expirable? def expirable?
[brouillon?, en_construction?, termine?].any? [brouillon?, en_construction?, termine? && procedure.feature_enabled?(:procedure_process_expired_dossiers_termine)].any?
end end
def approximative_expiration_date_reference def approximative_expiration_date_reference

View file

@ -234,7 +234,10 @@ class Procedure < ApplicationRecord
validates :description, presence: true, allow_blank: false, allow_nil: false validates :description, presence: true, allow_blank: false, allow_nil: false
validates :administrateurs, presence: true validates :administrateurs, presence: true
validates :lien_site_web, presence: true, if: :publiee? validates :lien_site_web, presence: true, if: :publiee?
validates :draft_revision, 'revisions/no_empty_repetitions': true, if: :validate_for_publication? validates :draft_revision,
'revisions/no_empty_repetition': true,
'revisions/no_empty_drop_down': true,
if: :validate_for_publication?
validate :check_juridique validate :check_juridique
validates :path, presence: true, format: { with: /\A[a-z0-9_\-]{3,200}\z/ }, uniqueness: { scope: [:path, :closed_at, :hidden_at, :unpublished_at], case_sensitive: false } validates :path, presence: true, format: { with: /\A[a-z0-9_\-]{3,200}\z/ }, uniqueness: { scope: [:path, :closed_at, :hidden_at, :unpublished_at], case_sensitive: false }
validates :duree_conservation_dossiers_dans_ds, allow_nil: false, numericality: { only_integer: true, greater_than_or_equal_to: 1, less_than_or_equal_to: MAX_DUREE_CONSERVATION } validates :duree_conservation_dossiers_dans_ds, allow_nil: false, numericality: { only_integer: true, greater_than_or_equal_to: 1, less_than_or_equal_to: MAX_DUREE_CONSERVATION }

View file

@ -0,0 +1,22 @@
class Revisions::NoEmptyDropDownValidator < ActiveModel::EachValidator
def validate_each(procedure, attribute, revision)
return if revision.nil?
tdcs = revision.types_de_champ + revision.types_de_champ_private
drop_downs = tdcs.filter(&:drop_down_list?)
drop_downs.each do |drop_down|
validate_drop_down_not_empty(procedure, attribute, drop_down)
end
end
private
def validate_drop_down_not_empty(procedure, attribute, drop_down)
if drop_down.drop_down_list_enabled_non_empty_options.empty?
procedure.errors.add(
attribute,
procedure.errors.generate_message(attribute, :empty_drop_down, { value: drop_down.libelle })
)
end
end
end

View file

@ -1,4 +1,4 @@
class Revisions::NoEmptyRepetitionsValidator < ActiveModel::EachValidator class Revisions::NoEmptyRepetitionValidator < ActiveModel::EachValidator
def validate_each(procedure, attribute, revision) def validate_each(procedure, attribute, revision)
return if revision.nil? return if revision.nil?

View file

@ -1,14 +1,25 @@
- if dossier.expirable? && dossier.close_to_expiration? -# small expires mention
.card.warning.mt-2.mb-3 - if dossier.expirable?
.card-title= t('shared.dossiers.header.banner.title') %p.expires_at
%p %small= t("shared.dossiers.header.expires_at.#{dossier.state}", date: safe_expiration_date(dossier))
- if dossier.brouillon? -# big banner warning
= t('shared.dossiers.header.banner.states.brouillon') - if dossier.close_to_expiration?
- elsif dossier.en_construction? .card.warning.mt-2.mb-3
= t('shared.dossiers.header.banner.states.en_construction') .card-title= t('shared.dossiers.header.banner.title')
- elsif dossier.termine? %p
= t('shared.dossiers.header.banner.states.termine') - if dossier.brouillon?
= t('shared.dossiers.header.banner.states.brouillon')
- elsif dossier.en_construction?
= t('shared.dossiers.header.banner.states.en_construction')
- elsif dossier.termine?
= t('shared.dossiers.header.banner.states.termine')
- if dossier.expiration_can_be_extended?
%br
= button_to t('shared.dossiers.header.banner.button_delay_expiration'), users_dossier_repousser_expiration_path(dossier), class: 'button secondary mt-2'
- else
%p.expires_at_en_instruction
%small= t("shared.dossiers.header.expires_at.en_instruction")
- if dossier.expiration_can_be_extended?
%br
= button_to t('shared.dossiers.header.banner.button_delay_expiration'), users_dossier_repousser_expiration_path(dossier), class: 'button secondary mt-2'

View file

@ -6,7 +6,7 @@
= t('views.users.dossiers.show.header.dossier_number', dossier_id: dossier.id) = t('views.users.dossiers.show.header.dossier_number', dossier_id: dossier.id)
= t('views.users.dossiers.show.header.created_date', date_du_dossier: I18n.l(dossier.created_at)) = t('views.users.dossiers.show.header.created_date', date_du_dossier: I18n.l(dossier.created_at))
= render(partial: 'shared/dossiers/short_expires_message', locals: {dossier: dossier}) = render(partial: 'shared/dossiers/expiration_banner', locals: {dossier: dossier})
.header-actions .header-actions
- if current_user.owns?(dossier) - if current_user.owns?(dossier)

View file

@ -1,9 +0,0 @@
- if dossier.expirable?
%p.expires_at
%small= t("shared.dossiers.header.expires_at.#{dossier.state}", date: safe_expiration_date(dossier))
- else
%p.expires_at_en_instruction
%small= t("shared.dossiers.header.expires_at.en_instruction")
= render(partial: 'shared/dossiers/expiration_banner', locals: {dossier: dossier})

View file

@ -75,23 +75,6 @@
.chart.cumulative-dossiers-chart.hidden .chart.cumulative-dossiers-chart.hidden
= area_chart @dossiers_cumulative = area_chart @dossiers_cumulative
- if super_admin_signed_in?
.stat-card.stat-card-half.pull-left
%span.stat-card-title Temps de traitement moyen dun dossier
.chart-container
.chart
= line_chart @dossier_instruction_mean_time,
:ytitle => "Jours"
.stat-card.stat-card-half.pull-left
%span.stat-card-title Temps de remplissage moyen dun dossier
.chart-container
.chart
= line_chart @dossier_filling_mean_time,
:ytitle => "Minutes"
.clearfix .clearfix
- if super_admin_signed_in? - if super_admin_signed_in?

View file

@ -10,7 +10,7 @@
- if dossier.en_construction_at.present? - if dossier.en_construction_at.present?
= t('views.users.dossiers.show.header.submit_date', date_du_dossier: I18n.l(dossier.en_construction_at)) = t('views.users.dossiers.show.header.submit_date', date_du_dossier: I18n.l(dossier.en_construction_at))
= render(partial: 'shared/dossiers/short_expires_message', locals: {dossier: dossier}) = render(partial: 'shared/dossiers/expiration_banner', locals: {dossier: dossier})
- if current_user.owns?(dossier) - if current_user.owns?(dossier)

View file

@ -63,8 +63,8 @@ module TPS
end end
config.middleware.use Rack::Attack config.middleware.use Rack::Attack
config.middleware.use Flipper::Middleware::Memoizer, # Ensure we make maximum one call per feature per request.
preload: [:instructeur_bypass_email_login_token] config.middleware.use Flipper::Middleware::Memoizer
config.ds_env = ENV.fetch('DS_ENV', Rails.env) config.ds_env = ENV.fetch('DS_ENV', Rails.env)

View file

@ -30,7 +30,6 @@ features = [
:dossier_pdf_vide, :dossier_pdf_vide,
:expert_not_allowed_to_invite, :expert_not_allowed_to_invite,
:hide_instructeur_email, :hide_instructeur_email,
:instructeur_bypass_email_login_token,
:procedure_revisions, :procedure_revisions,
:procedure_routage_api, :procedure_routage_api,
:procedure_process_expired_dossiers_termine :procedure_process_expired_dossiers_termine

View file

@ -311,6 +311,7 @@ fr:
forbidden_html: "Seul-e-s les usagers peuvent se connecter via France Connect. En tant quinstructeur ou administrateur, nous vous invitons à <a href='%{reset_link}'>réininitialiser votre mot de passe</a>." forbidden_html: "Seul-e-s les usagers peuvent se connecter via France Connect. En tant quinstructeur ou administrateur, nous vous invitons à <a href='%{reset_link}'>réininitialiser votre mot de passe</a>."
procedure_archived: "Cette démarche en ligne a été close, il nest plus possible de déposer de dossier." procedure_archived: "Cette démarche en ligne a été close, il nest plus possible de déposer de dossier."
empty_repetition: 'Le bloc répétable « %{value} » doit comporter au moins un champ' empty_repetition: 'Le bloc répétable « %{value} » doit comporter au moins un champ'
empty_drop_down: 'La liste de choix « %{value} » doit comporter au moins un choix sélectionnable'
# procedure_not_draft: "Cette démarche nest maintenant plus en brouillon." # procedure_not_draft: "Cette démarche nest maintenant plus en brouillon."
cadastres_empty: cadastres_empty:
one: "Aucune parcelle cadastrale sur la zone sélectionnée" one: "Aucune parcelle cadastrale sur la zone sélectionnée"

View file

@ -165,7 +165,7 @@ describe ApplicationController, type: :controller do
let(:sensitive_path) { true } let(:sensitive_path) { true }
before do before do
Flipper.disable(:instructeur_bypass_email_login_token) current_instructeur.update!(bypass_email_login_token: false)
end end
context 'when the instructeur is signed_in' do context 'when the instructeur is signed_in' do

View file

@ -90,111 +90,4 @@ describe StatsController, type: :controller do
end end
end end
end end
describe "#dossier_instruction_mean_time" do
# Month-2: mean 3 days
# procedure_1: mean 2 days
# dossier_p1_a: 3 days
# dossier_p1_b: 1 days
# procedure_2: mean 4 days
# dossier_p2_a: 4 days
#
# Month-1: mean 5 days
# procedure_1: mean 5 days
# dossier_p1_c: 5 days
before do
procedure_1 = create(:procedure)
procedure_2 = create(:procedure)
dossier_p1_a = create(:dossier, :accepte,
procedure: procedure_1,
en_construction_at: 2.months.ago.beginning_of_month,
processed_at: 2.months.ago.beginning_of_month + 3.days)
dossier_p1_b = create(:dossier, :accepte,
procedure: procedure_1,
en_construction_at: 2.months.ago.beginning_of_month,
processed_at: 2.months.ago.beginning_of_month + 1.day)
dossier_p1_c = create(:dossier, :accepte,
procedure: procedure_1,
en_construction_at: 1.month.ago.beginning_of_month,
processed_at: 1.month.ago.beginning_of_month + 5.days)
dossier_p2_a = create(:dossier, :accepte,
procedure: procedure_2,
en_construction_at: 2.months.ago.beginning_of_month,
processed_at: 2.months.ago.beginning_of_month + 4.days)
@expected_hash = {
(2.months.ago.beginning_of_month).to_s => 3.0,
(1.month.ago.beginning_of_month).to_s => 5.0
}
end
let (:association) { Dossier.state_not_brouillon }
subject { StatsController.new.send(:dossier_instruction_mean_time, association) }
it { expect(subject).to eq(@expected_hash) }
end
describe "#dossier_filling_mean_time" do
# Month-2: mean 30 minutes
# procedure_1: mean 20 minutes
# dossier_p1_a: 30 minutes
# dossier_p1_b: 10 minutes
# procedure_2: mean 40 minutes
# dossier_p2_a: 80 minutes, for twice the fields
#
# Month-1: mean 50 minutes
# procedure_1: mean 50 minutes
# dossier_p1_c: 50 minutes
before do
procedure_1 = create(:procedure, :with_type_de_champ, types_de_champ_count: 24)
procedure_2 = create(:procedure, :with_type_de_champ, types_de_champ_count: 48)
dossier_p1_a = create(:dossier, :accepte,
procedure: procedure_1,
created_at: 2.months.ago.beginning_of_month,
en_construction_at: 2.months.ago.beginning_of_month + 30.minutes,
processed_at: 2.months.ago.beginning_of_month + 1.day)
dossier_p1_b = create(:dossier, :accepte,
procedure: procedure_1,
created_at: 2.months.ago.beginning_of_month,
en_construction_at: 2.months.ago.beginning_of_month + 10.minutes,
processed_at: 2.months.ago.beginning_of_month + 1.day)
dossier_p1_c = create(:dossier, :accepte,
procedure: procedure_1,
created_at: 1.month.ago.beginning_of_month,
en_construction_at: 1.month.ago.beginning_of_month + 50.minutes,
processed_at: 1.month.ago.beginning_of_month + 1.day)
dossier_p2_a = create(:dossier, :accepte,
procedure: procedure_2,
created_at: 2.months.ago.beginning_of_month,
en_construction_at: 2.months.ago.beginning_of_month + 80.minutes,
processed_at: 2.months.ago.beginning_of_month + 1.day)
@expected_hash = {
(2.months.ago.beginning_of_month).to_s => 30.0,
(1.month.ago.beginning_of_month).to_s => 50.0
}
end
let (:association) { Dossier.state_not_brouillon }
subject { StatsController.new.send(:dossier_filling_mean_time, association) }
it { expect(subject).to eq(@expected_hash) }
end
describe '#avis_usage' do
let!(:dossier) { create(:dossier) }
let!(:avis_with_dossier) { create(:avis) }
let!(:dossier2) { create(:dossier) }
before { Timecop.freeze(Time.zone.now) }
after { Timecop.return }
subject { StatsController.new.send(:avis_usage) }
it { expect(subject).to match([[3.weeks.ago.to_i, 0], [2.weeks.ago.to_i, 0], [1.week.ago.to_i, 33.33]]) }
end
end end

View file

@ -2,6 +2,8 @@ FactoryBot.define do
sequence(:instructeur_email) { |n| "inst#{n}@inst.com" } sequence(:instructeur_email) { |n| "inst#{n}@inst.com" }
factory :instructeur do factory :instructeur do
bypass_email_login_token { true }
user { association :user, email: email, password: password } user { association :user, email: email, password: password }
transient do transient do

View file

@ -89,6 +89,9 @@ FactoryBot.define do
trait :long do trait :long do
drop_down_list_value { "alpha\r\nbravo\r\n--separateur--\r\ncharly\r\ndelta\r\necho\r\nfox-trot\r\ngolf" } drop_down_list_value { "alpha\r\nbravo\r\n--separateur--\r\ncharly\r\ndelta\r\necho\r\nfox-trot\r\ngolf" }
end end
trait :without_selectable_values do
drop_down_list_value { "\r\n--separateur--\r\n--separateur 2--\r\n \r\n" }
end
end end
factory :type_de_champ_multiple_drop_down_list do factory :type_de_champ_multiple_drop_down_list do
type_champ { TypeDeChamp.type_champs.fetch(:multiple_drop_down_list) } type_champ { TypeDeChamp.type_champs.fetch(:multiple_drop_down_list) }

View file

@ -282,13 +282,17 @@ describe Procedure do
describe 'draft_revision' do describe 'draft_revision' do
let(:repetition) { build(:type_de_champ_repetition, libelle: 'Enfants') } let(:repetition) { build(:type_de_champ_repetition, libelle: 'Enfants') }
let(:text_field) { build(:type_de_champ_text) } let(:text_field) { build(:type_de_champ_text) }
let(:procedure) { create(:procedure, types_de_champ: [repetition]) }
let(:invalid_repetition_error_message) { 'Le bloc répétable « Enfants » doit comporter au moins un champ' } let(:invalid_repetition_error_message) { 'Le bloc répétable « Enfants » doit comporter au moins un champ' }
let(:drop_down) { build(:type_de_champ_drop_down_list, :without_selectable_values, libelle: 'Civilité') }
let(:invalid_drop_down_error_message) { 'La liste de choix « Civilité » doit comporter au moins un choix sélectionnable' }
let(:procedure) { create(:procedure, types_de_champ: [repetition, drop_down]) }
context 'on a draft procedure' do context 'on a draft procedure' do
it 'doesnt validate repetitions' do it 'doesnt validate the draft revision' do
procedure.validate procedure.validate
expect(procedure.errors[:draft_revision]).not_to include(invalid_repetition_error_message) expect(procedure.errors[:draft_revision]).not_to be_present
end end
end end
@ -306,6 +310,15 @@ describe Procedure do
procedure.validate procedure.validate
expect(procedure.errors.full_messages_for(:draft_revision)).not_to include(invalid_repetition_error_message) expect(procedure.errors.full_messages_for(:draft_revision)).not_to include(invalid_repetition_error_message)
end end
it 'validates that no drop-down type de champ is empty' do
procedure.validate
expect(procedure.errors.full_messages_for(:draft_revision)).to include(invalid_drop_down_error_message)
drop_down.update!(drop_down_list_value: "--title--\r\nsome value")
procedure.reload.validate
expect(procedure.errors.full_messages_for(:draft_revision)).not_to include(invalid_drop_down_error_message)
end
end end
context 'when validating for publication' do context 'when validating for publication' do
@ -313,6 +326,11 @@ describe Procedure do
procedure.validate(:publication) procedure.validate(:publication)
expect(procedure.errors.full_messages_for(:draft_revision)).to include(invalid_repetition_error_message) expect(procedure.errors.full_messages_for(:draft_revision)).to include(invalid_repetition_error_message)
end end
it 'validates that no drop-down type de champ is empty' do
procedure.validate(:publication)
expect(procedure.errors.full_messages_for(:draft_revision)).to include(invalid_drop_down_error_message)
end
end end
end end
end end

View file

@ -1,3 +1,5 @@
include ActiveSupport::Testing::TimeHelpers
describe Stat do describe Stat do
describe '.deleted_dossiers_states' do describe '.deleted_dossiers_states' do
subject { Stat.send(:deleted_dossiers_states) } subject { Stat.send(:deleted_dossiers_states) }
@ -78,19 +80,21 @@ describe Stat do
describe '.last_four_months_hash' do describe '.last_four_months_hash' do
it 'works count and cumulate counters by month for both dossier and deleted dossiers' do it 'works count and cumulate counters by month for both dossier and deleted dossiers' do
4.downto(1).map do |i| travel_to Time.zone.local(2021, 11, 25) do
create(:dossier, state: :en_construction, en_construction_at: i.months.ago) 4.downto(1).map do |i|
create(:deleted_dossier, dossier_id: i + 100, state: :en_construction, deleted_at: i.month.ago) create(:dossier, state: :en_construction, en_construction_at: i.months.ago)
create(:deleted_dossier, dossier_id: i + 100, state: :en_construction, deleted_at: i.month.ago)
end
rs = Stat.send(:last_four_months_hash, [
[Dossier.state_not_brouillon, :en_construction_at],
[DeletedDossier.where.not(state: :brouillon), :deleted_at]
])
expect(rs).to eq([
["août 2021", 2],
["septembre 2021", 2],
["octobre 2021", 2]
])
end end
rs = Stat.send(:last_four_months_hash, [
[Dossier.state_not_brouillon, :en_construction_at],
[DeletedDossier.where.not(state: :brouillon), :deleted_at]
])
expect(rs).to eq([
["août 2021", 2],
["septembre 2021", 2],
["octobre 2021", 2]
])
end end
end end

View file

@ -89,10 +89,6 @@ RSpec.configure do |config|
Geocoder.configure(lookup: :test) Geocoder.configure(lookup: :test)
end end
config.before(:each) do
Flipper.enable(:instructeur_bypass_email_login_token)
end
# By default, forgery protection is disabled in the test environment. # By default, forgery protection is disabled in the test environment.
# (See `config.action_controller.allow_forgery_protection` in `config/test.rb`) # (See `config.action_controller.allow_forgery_protection` in `config/test.rb`)
# #

View file

@ -17,7 +17,7 @@ module SystemHelpers
fill_in :user_password, with: password fill_in :user_password, with: password
if sign_in_by_link if sign_in_by_link
Flipper.disable(:instructeur_bypass_email_login_token) User.find_by(email: email)&.instructeur&.update!(bypass_email_login_token: false)
end end
perform_enqueued_jobs do perform_enqueued_jobs do

View file

@ -166,6 +166,8 @@ describe 'The user' do
end end
scenario 'extends dossier experation date more than one time, ', js: true do scenario 'extends dossier experation date more than one time, ', js: true do
Flipper.enable(:procedure_process_expired_dossiers_termine)
allow(simple_procedure).to receive(:feature_enabled?).with(:procedure_process_expired_dossiers_termine).and_return(true)
user_old_dossier = create(:dossier, user_old_dossier = create(:dossier,
procedure: simple_procedure, procedure: simple_procedure,
created_at: simple_procedure.duree_conservation_dossiers_dans_ds.month.ago, created_at: simple_procedure.duree_conservation_dossiers_dans_ds.month.ago,

View file

@ -0,0 +1,68 @@
describe 'shared/dossiers/expiration_banner.html.haml', type: :view do
include DossierHelper
let(:dossier) { build(:dossier, state, attributes.merge(id: 1, state: state)) }
let(:i18n_key_state) { state }
subject do
render('shared/dossiers/expiration_banner.html.haml',
dossier: dossier,
current_user: build(:user))
end
context 'with procedure having procedure_process_expired_dossiers_termine not enabled' do
before { allow(dossier.procedure).to receive(:feature_enabled?).with(:procedure_process_expired_dossiers_termine).and_return(false) }
let(:attributes) { { processed_at: 6.months.ago } }
let(:state) { :accepte }
it 'render estimated expiration date' do
expect(subject).not_to have_selector('.expires_at')
end
end
context 'with procedure having procedure_process_expired_dossiers_termine enabled' do
before { allow(dossier.procedure).to receive(:feature_enabled?).with(:procedure_process_expired_dossiers_termine).and_return(true) }
context 'with dossier.brouillon?' do
let(:attributes) { { created_at: 6.months.ago } }
let(:state) { :brouillon }
it 'render estimated expiration date' do
expect(subject).to have_selector('.expires_at',
text: I18n.t("shared.dossiers.header.expires_at.#{i18n_key_state}",
date: safe_expiration_date(dossier)))
end
end
context 'with dossier.en_construction?' do
let(:attributes) { { en_construction_at: 6.months.ago } }
let(:state) { :en_construction }
it 'render estimated expiration date' do
expect(subject).to have_selector('.expires_at',
text: I18n.t("shared.dossiers.header.expires_at.#{i18n_key_state}",
date: safe_expiration_date(dossier)))
end
end
context 'with dossier.en_instruction?' do
let(:state) { :en_instruction }
let(:attributes) { {} }
it 'render estimated expiration date' do
expect(subject).to have_selector('p.expires_at_en_instruction',
text: I18n.t("shared.dossiers.header.expires_at.#{i18n_key_state}"))
end
end
context 'with dossier.en_processed_at?' do
let(:state) { :accepte }
let(:attributes) { {} }
it 'render estimated expiration date' do
allow(dossier).to receive(:processed_at).and_return(6.months.ago)
expect(subject).to have_selector('.expires_at',
text: I18n.t("shared.dossiers.header.expires_at.#{i18n_key_state}",
date: safe_expiration_date(dossier)))
end
end
end
end

View file

@ -1,56 +0,0 @@
describe 'shared/dossiers/short_expires_message.html.haml', type: :view do
include DossierHelper
let(:dossier) do
build(:dossier, state, attributes.merge(id: 1, state: state))
end
let(:i18n_key_state) { state }
subject do
render('shared/dossiers/short_expires_message.html.haml',
dossier: dossier,
current_user: build(:user))
end
context 'with dossier.brouillon?' do
let(:attributes) { { created_at: 6.months.ago } }
let(:state) { :brouillon }
it 'render estimated expiration date' do
expect(subject).to have_selector('.expires_at',
text: I18n.t("shared.dossiers.header.expires_at.#{i18n_key_state}",
date: safe_expiration_date(dossier)))
end
end
context 'with dossier.en_construction?' do
let(:attributes) { { en_construction_at: 6.months.ago } }
let(:state) { :en_construction }
it 'render estimated expiration date' do
expect(subject).to have_selector('.expires_at',
text: I18n.t("shared.dossiers.header.expires_at.#{i18n_key_state}",
date: safe_expiration_date(dossier)))
end
end
context 'with dossier.en_instruction?' do
let(:state) { :en_instruction }
let(:attributes) { {} }
it 'render estimated expiration date' do
expect(subject).to have_selector('p.expires_at_en_instruction',
text: I18n.t("shared.dossiers.header.expires_at.#{i18n_key_state}"))
end
end
context 'with dossier.en_processed_at?' do
let(:state) { :accepte }
let(:attributes) { {} }
it 'render estimated expiration date' do
allow(dossier).to receive(:processed_at).and_return(6.months.ago)
expect(subject).to have_selector('.expires_at',
text: I18n.t("shared.dossiers.header.expires_at.#{i18n_key_state}",
date: safe_expiration_date(dossier)))
end
end
end