Merge pull request #4945 from betagouv/dev

2020-03-26-01
This commit is contained in:
Paul Chavard 2020-03-26 10:48:27 +01:00 committed by GitHub
commit 1aa4936b94
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
75 changed files with 854 additions and 185 deletions

View file

@ -78,7 +78,7 @@ GEM
tzinfo (~> 1.1) tzinfo (~> 1.1)
addressable (2.7.0) addressable (2.7.0)
public_suffix (>= 2.0.2, < 5.0) public_suffix (>= 2.0.2, < 5.0)
administrate (0.12.0) administrate (0.13.0)
actionpack (>= 4.2) actionpack (>= 4.2)
actionview (>= 4.2) actionview (>= 4.2)
activerecord (>= 4.2) activerecord (>= 4.2)
@ -212,7 +212,7 @@ GEM
activesupport (>= 3.0.0) activesupport (>= 3.0.0)
faraday (0.15.4) faraday (0.15.4)
multipart-post (>= 1.2, < 3) multipart-post (>= 1.2, < 3)
ffi (1.9.25) ffi (1.12.2)
flipper (0.17.2) flipper (0.17.2)
flipper-active_record (0.17.2) flipper-active_record (0.17.2)
activerecord (>= 4.2, < 7) activerecord (>= 4.2, < 7)
@ -588,10 +588,9 @@ GEM
sass-listen (4.0.0) sass-listen (4.0.0)
rb-fsevent (~> 0.9, >= 0.9.4) rb-fsevent (~> 0.9, >= 0.9.4)
rb-inotify (~> 0.9, >= 0.9.7) rb-inotify (~> 0.9, >= 0.9.7)
sassc (2.0.0) sassc (2.2.1)
ffi (~> 1.9.6) ffi (~> 1.9)
rake sassc-rails (2.1.2)
sassc-rails (2.1.0)
railties (>= 4.0.0) railties (>= 4.0.0)
sassc (>= 2.0) sassc (>= 2.0)
sprockets (> 3.0) sprockets (> 3.0)

View file

@ -68,17 +68,19 @@ En local, un utilisateur de test est créé automatiquement, avec les identifian
### Programmation des jobs ### Programmation des jobs
AutoArchiveProcedureJob.set(cron: "* * * * *").perform_later AutoArchiveProcedureJob.set(cron: "* * * * *").perform_later
WeeklyOverviewJob.set(cron: "0 7 * * 1").perform_later WeeklyOverviewJob.set(cron: "0 7 * * MON").perform_later
DeclarativeProceduresJob.set(cron: "* * * * *").perform_later DeclarativeProceduresJob.set(cron: "* * * * *").perform_later
UpdateAdministrateurUsageStatisticsJob.set(cron: "0 10 * * *").perform_later UpdateAdministrateurUsageStatisticsJob.set(cron: "0 10 * * *").perform_later
FindDubiousProceduresJob.set(cron: "0 0 * * *").perform_later FindDubiousProceduresJob.set(cron: "0 0 * * *").perform_later
Administrateurs::ActivateBeforeExpirationJob.set(cron: "0 8 * * *").perform_later Administrateurs::ActivateBeforeExpirationJob.set(cron: "0 8 * * *").perform_later
WarnExpiringDossiersJob.set(cron: "0 0 1 * *").perform_later WarnExpiringDossiersJob.set(cron: "0 0 1 * *").perform_later
InstructeurEmailNotificationJob.set(cron: "0 10 * * 1,2,3,4,5,6").perform_later InstructeurEmailNotificationJob.set(cron: "0 10 * * MON-FRI").perform_later
PurgeUnattachedBlobsJob.set(cron: "0 0 * * *").perform_later PurgeUnattachedBlobsJob.set(cron: "0 0 * * *").perform_later
OperationsSignatureJob.set(cron: "0 6 * * *").perform_later OperationsSignatureJob.set(cron: "0 6 * * *").perform_later
SeekAndDestroyExpiredDossiersJob.set(cron: "0 7 * * *").perform_later ExpiredDossiersDeletionJob.set(cron: "0 7 * * *").perform_later
PurgeStaleExportsJob.set(cron: "*/5 * * * *").perform_later PurgeStaleExportsJob.set(cron: "*/5 * * * *").perform_later
NotifyDraftNotSubmittedJob.set(cron: "0 7 * * *").perform_later
DiscardedDossiersDeletionJob.set(cron: "0 7 * * *").perform_later
### Voir les emails envoyés en local ### Voir les emails envoyés en local

View file

@ -32,6 +32,10 @@
display: block; display: block;
} }
.deleted-cell {
padding: (2 * $default-spacer) $default-spacer;
}
.icon.folder { .icon.folder {
position: relative; position: relative;

View file

@ -44,6 +44,12 @@
} }
} }
.form-label {
font-weight: bold;
font-size: 18px;
margin-bottom: $default-padding;
}
.notice { .notice {
@include notice-text-style; @include notice-text-style;
margin-top: - $default-spacer; margin-top: - $default-spacer;

View file

@ -26,7 +26,7 @@
input[type=email] { input[type=email] {
width: auto; width: auto;
margin-bottom: 0; margin-bottom: $default-spacer;
} }
.button { .button {

View file

@ -17,21 +17,41 @@
border: 1px solid $blue; border: 1px solid $blue;
} }
&.en-instruction {
@extend .instruction;
}
&.construction { &.construction {
background-color: #FFFFFF; background-color: #FFFFFF;
color: $black; color: $black;
border: 1px solid $black; border: 1px solid $black;
} }
&.en-construction {
@extend .construction;
}
&.accepted { &.accepted {
background-color: $green; background-color: $green;
} }
&.accepte {
@extend .accepted;
}
&.refused { &.refused {
background-color: $dark-red; background-color: $dark-red;
} }
&.refuse {
@extend .refused;
}
&.without-continuation { &.without-continuation {
background-color: $black; background-color: $black;
} }
&.sans-suite {
@extend .without-continuation;
}
} }

View file

@ -2,6 +2,7 @@
@import "common"; @import "common";
@import "constants"; @import "constants";
@import "mixins"; @import "mixins";
@import "utils";
$header-landing-breakpoint: 1040px; $header-landing-breakpoint: 1040px;
$header-mobile-breakpoint: 550px; $header-mobile-breakpoint: 550px;
@ -148,6 +149,10 @@ $header-mobile-breakpoint: 550px;
margin: 0; margin: 0;
} }
label.hidden {
@extend .hidden;
}
button { button {
@extend %outline; @extend %outline;

View file

@ -16,6 +16,10 @@
margin-bottom: 1 * $default-padding; margin-bottom: 1 * $default-padding;
} }
.titre-dossiers {
text-align: center;
}
.dossiers-table { .dossiers-table {
margin-top: $default-spacer; margin-top: $default-spacer;
margin-bottom: 3 * $default-spacer; margin-bottom: 3 * $default-spacer;
@ -30,6 +34,11 @@
} }
} }
.afficher-dossiers-supprimes {
display: flex;
justify-content: flex-end;
}
.filter { .filter {
display: inline-block; display: inline-block;
padding-left: 10px; padding-left: 10px;
@ -48,7 +57,7 @@
display: inline-block; display: inline-block;
} }
p.explication-onglet { .explication-onglet {
margin-bottom: 3 * $default-spacer; margin-bottom: 3 * $default-spacer;
text-align: center; text-align: center;
} }

View file

@ -113,6 +113,14 @@ module Instructeurs
assign_exports assign_exports
end end
def deleted_dossiers
@procedure = procedure
@deleted_dossiers = @procedure
.deleted_dossiers.where.not(state: :brouillon)
.order(:dossier_id)
.page params[:page]
end
def update_displayed_fields def update_displayed_fields
values = params[:values] values = params[:values]

View file

@ -22,7 +22,7 @@ module Manager
def hide def hide
dossier = Dossier.find(params[:id]) dossier = Dossier.find(params[:id])
dossier.delete_and_keep_track(current_administration) dossier.delete_and_keep_track!(current_administration, :manager_request)
logger.info("Le dossier #{dossier.id} est supprimé par #{current_administration.email}") logger.info("Le dossier #{dossier.id} est supprimé par #{current_administration.email}")
flash[:notice] = "Le dossier #{dossier.id} a été supprimé." flash[:notice] = "Le dossier #{dossier.id} a été supprimé."

View file

@ -166,6 +166,12 @@ module Users
end end
end end
def extend_conservation
dossier.update(en_construction_conservation_extension: dossier.en_construction_conservation_extension + 1.month)
flash[:notice] = 'Votre dossier sera conservé un mois supplémentaire'
redirect_to dossier_path(@dossier)
end
def modifier def modifier
@dossier = dossier_with_champs @dossier = dossier_with_champs
end end
@ -203,7 +209,7 @@ module Users
dossier = current_user.dossiers.includes(:user, procedure: :administrateurs).find(params[:id]) dossier = current_user.dossiers.includes(:user, procedure: :administrateurs).find(params[:id])
if dossier.can_be_deleted_by_user? if dossier.can_be_deleted_by_user?
dossier.delete_and_keep_track(current_user) dossier.delete_and_keep_track!(current_user, :user_request)
flash.notice = 'Votre dossier a bien été supprimé.' flash.notice = 'Votre dossier a bien été supprimé.'
redirect_to dossiers_path redirect_to dossiers_path
else else

View file

@ -12,7 +12,6 @@ class ProcedureDashboard < Administrate::BaseDashboard
types_de_champ_private: TypesDeChampCollectionField, types_de_champ_private: TypesDeChampCollectionField,
path: ProcedureLinkField, path: ProcedureLinkField,
dossiers: Field::HasMany, dossiers: Field::HasMany,
instructeurs: Field::HasMany,
administrateurs: Field::HasMany, administrateurs: Field::HasMany,
id: Field::Number.with_options(searchable: true), id: Field::Number.with_options(searchable: true),
libelle: Field::String, libelle: Field::String,
@ -75,7 +74,6 @@ class ProcedureDashboard < Administrate::BaseDashboard
:types_de_champ_private, :types_de_champ_private,
:for_individual, :for_individual,
:auto_archive_on, :auto_archive_on,
:instructeurs,
:initiated_mail_template, :initiated_mail_template,
:received_mail_template, :received_mail_template,
:closed_mail_template, :closed_mail_template,

View file

@ -83,6 +83,24 @@ module DossierHelper
end end
end end
def status_badge(state)
status_text = dossier_display_state(state, lower: true)
status_class = state.tr('_', '-')
content_tag(:span, status_text, class: "label #{status_class} ")
end
def deletion_reason_badge(reason)
if reason.present?
status_text = I18n.t(reason, scope: [:activerecord, :attributes, :deleted_dossier, :reason])
status_class = reason.tr('_', '-')
else
status_text = I18n.t(:unknown, scope: [:activerecord, :attributes, :deleted_dossier, :reason])
status_class = 'unknown'
end
content_tag(:span, status_text, class: "label #{status_class} ")
end
private private
def dinum_instance? def dinum_instance?

View file

@ -2,7 +2,7 @@ import { fire, timeoutable } from '@utils';
// Manages a queue of Autosave operations, // Manages a queue of Autosave operations,
// and sends `autosave:*` events to indicate the state of the requests. // and sends `autosave:*` events to indicate the state of the requests.
export default class AutosaveController { export default class AutoSaveController {
constructor() { constructor() {
this.timeoutDelay = 60000; // 1mn this.timeoutDelay = 60000; // 1mn
this.latestPromise = Promise.resolve(); this.latestPromise = Promise.resolve();

View file

@ -1,4 +1,4 @@
import AutosaveController from './autosave-controller.js'; import AutoSaveController from './auto-save-controller.js';
import { import {
debounce, debounce,
delegate, delegate,
@ -14,7 +14,7 @@ const AUTOSAVE_DEBOUNCE_DELAY = gon.autosave.debounce_delay;
const AUTOSAVE_STATUS_VISIBLE_DURATION = gon.autosave.status_visible_duration; const AUTOSAVE_STATUS_VISIBLE_DURATION = gon.autosave.status_visible_duration;
// Create a controller responsible for queuing autosave operations. // Create a controller responsible for queuing autosave operations.
const autosaveController = new AutosaveController(); const autoSaveController = new AutoSaveController();
// Whenever a 'change' event is triggered on one of the form inputs, try to autosave. // Whenever a 'change' event is triggered on one of the form inputs, try to autosave.
@ -26,13 +26,13 @@ delegate(
formInputsSelector, formInputsSelector,
debounce(() => { debounce(() => {
const form = document.querySelector(formSelector); const form = document.querySelector(formSelector);
autosaveController.enqueueAutosaveRequest(form); autoSaveController.enqueueAutosaveRequest(form);
}, AUTOSAVE_DEBOUNCE_DELAY) }, AUTOSAVE_DEBOUNCE_DELAY)
); );
delegate('click', '.autosave-retry', () => { delegate('click', '.autosave-retry', () => {
const form = document.querySelector(formSelector); const form = document.querySelector(formSelector);
autosaveController.enqueueAutosaveRequest(form); autoSaveController.enqueueAutosaveRequest(form);
}); });
// Display some UI during the autosave // Display some UI during the autosave

View file

@ -16,13 +16,13 @@ import '../shared/franceconnect';
import '../shared/toggle-target'; import '../shared/toggle-target';
import '../new_design/dropdown'; import '../new_design/dropdown';
import '../new_design/autosave';
import '../new_design/form-validation'; import '../new_design/form-validation';
import '../new_design/procedure-context'; import '../new_design/procedure-context';
import '../new_design/procedure-form'; import '../new_design/procedure-form';
import '../new_design/select2'; import '../new_design/select2';
import '../new_design/spinner'; import '../new_design/spinner';
import '../new_design/support'; import '../new_design/support';
import '../new_design/dossiers/auto-save';
import '../new_design/champs/carte'; import '../new_design/champs/carte';
import '../new_design/champs/linked-drop-down-list'; import '../new_design/champs/linked-drop-down-list';

View file

@ -0,0 +1,8 @@
class DiscardedDossiersDeletionJob < ApplicationJob
queue_as :cron
def perform(*args)
Dossier.discarded_brouillon_expired.destroy_all
Dossier.discarded_en_construction_expired.destroy_all
end
end

View file

@ -0,0 +1,7 @@
class NotifyDraftNotSubmittedJob < ApplicationJob
queue_as :cron
def perform(*args)
Dossier.notify_draft_not_submitted
end
end

View file

@ -104,4 +104,11 @@ class DossierMailer < ApplicationMailer
mail(from: NO_REPLY_EMAIL, to: instructeur.email, subject: @subject) mail(from: NO_REPLY_EMAIL, to: instructeur.email, subject: @subject)
end end
def notify_brouillon_not_submitted(dossier)
@subject = "Attention : votre dossier n'est pas déposé."
@dossier = dossier
mail(to: dossier.user.email, subject: @subject)
end
end end

View file

@ -18,7 +18,7 @@ class Administrateur < ApplicationRecord
end end
def email def email
user.email user&.email
end end
# validate :password_complexity, if: Proc.new { |a| Devise.password_length.include?(a.password.try(:size)) } # validate :password_complexity, if: Proc.new { |a| Devise.password_length.include?(a.password.try(:size)) }

View file

@ -80,6 +80,10 @@ class Champ < ApplicationRecord
type_de_champ.to_typed_id type_de_champ.to_typed_id
end end
def html_label?
true
end
private private
def needs_dossier_id? def needs_dossier_id?

View file

@ -1,2 +1,5 @@
class Champs::CiviliteChamp < Champ class Champs::CiviliteChamp < Champ
def html_label?
false
end
end end

View file

@ -13,6 +13,10 @@ class Champs::DatetimeChamp < Champ
value.present? ? I18n.l(Time.zone.parse(value)) : "" value.present? ? I18n.l(Time.zone.parse(value)) : ""
end end
def html_label?
false
end
private private
def format_before_save def format_before_save

View file

@ -2,8 +2,8 @@ class Champs::DecimalNumberChamp < Champ
validates :value, numericality: { validates :value, numericality: {
allow_nil: true, allow_nil: true,
allow_blank: true, allow_blank: true,
message: -> (object, data) { message: -> (object, _data) {
"« #{object.libelle} » " + object.errors.generate_message(data[:attribute].downcase, :not_a_number) "« #{object.libelle} » " + object.errors.generate_message(:value, :not_a_number)
} }
} }

View file

@ -3,8 +3,8 @@ class Champs::IntegerNumberChamp < Champ
only_integer: true, only_integer: true,
allow_nil: true, allow_nil: true,
allow_blank: true, allow_blank: true,
message: -> (object, data) { message: -> (object, _data) {
"« #{object.libelle} » " + object.errors.generate_message(data[:attribute].downcase, :not_an_integer) "« #{object.libelle} » " + object.errors.generate_message(:value, :not_an_integer)
} }
} }

View file

@ -16,6 +16,10 @@ class Champs::PieceJustificativeChamp < Champ
"image/jpeg" "image/jpeg"
] ]
def main_value_name
:piece_justificative_file
end
def search_terms def search_terms
# We dont know how to search inside documents yet # We dont know how to search inside documents yet
end end

View file

@ -1,7 +1,20 @@
class DeletedDossier < ApplicationRecord class DeletedDossier < ApplicationRecord
belongs_to :procedure belongs_to :procedure
def self.create_from_dossier(dossier) enum reason: {
DeletedDossier.create!(dossier_id: dossier.id, procedure: dossier.procedure, state: dossier.state, deleted_at: Time.zone.now) user_request: 'user_request',
manager_request: 'manager_request',
user_removed: 'user_removed',
expired: 'expired'
}
def self.create_from_dossier(dossier, reason)
create!(
reason: reasons.fetch(reason),
dossier_id: dossier.id,
procedure: dossier.procedure,
state: dossier.state,
deleted_at: Time.zone.now
)
end end
end end

View file

@ -22,6 +22,11 @@ class Dossier < ApplicationRecord
TAILLE_MAX_ZIP = 50.megabytes TAILLE_MAX_ZIP = 50.megabytes
REMAINING_DAYS_BEFORE_CLOSING = 2
INTERVAL_BEFORE_CLOSING = "#{REMAINING_DAYS_BEFORE_CLOSING} days"
INTERVAL_BEFORE_EXPIRATION = '1 month'
INTERVAL_EXPIRATION = '1 month 5 days'
has_one :etablissement, dependent: :destroy has_one :etablissement, dependent: :destroy
has_one :individual, validate: false, dependent: :destroy has_one :individual, validate: false, dependent: :destroy
has_one :attestation, dependent: :destroy has_one :attestation, dependent: :destroy
@ -38,7 +43,7 @@ class Dossier < ApplicationRecord
has_many :previous_followers_instructeurs, -> { distinct }, through: :previous_follows, source: :instructeur has_many :previous_followers_instructeurs, -> { distinct }, through: :previous_follows, source: :instructeur
has_many :avis, inverse_of: :dossier, dependent: :destroy has_many :avis, inverse_of: :dossier, dependent: :destroy
has_many :dossier_operation_logs, dependent: :destroy has_many :dossier_operation_logs, dependent: :nullify
belongs_to :groupe_instructeur belongs_to :groupe_instructeur
has_one :procedure, through: :groupe_instructeur has_one :procedure, through: :groupe_instructeur
@ -164,28 +169,67 @@ class Dossier < ApplicationRecord
user: []) user: [])
} }
scope :brouillon_close_to_expiration, -> do scope :with_notifiable_procedure, -> do
brouillon joins(:procedure)
.joins(:procedure) .where.not(procedures: { aasm_state: :brouillon })
.where("dossiers.created_at + (duree_conservation_dossiers_dans_ds * interval '1 month') - INTERVAL '1 month' <= now()")
end
scope :en_construction_close_to_expiration, -> do
en_construction
.joins(:procedure)
.where("dossiers.en_construction_at + (duree_conservation_dossiers_dans_ds * interval '1 month') - INTERVAL '1 month' <= now()")
end
scope :en_instruction_close_to_expiration, -> do
en_instruction
.joins(:procedure)
.where("dossiers.en_instruction_at + (duree_conservation_dossiers_dans_ds * interval '1 month') - INTERVAL '1 month' <= now()")
end end
scope :brouillon_expired, -> { brouillon.where("brouillon_close_to_expiration_notice_sent_at < (now() - INTERVAL '1 month 5 days')") } scope :brouillon_close_to_expiration, -> do
scope :en_construction_expired, -> { en_construction.where("en_construction_close_to_expiration_notice_sent_at < (now() - INTERVAL '1 month 5 days')") } state_brouillon
.joins(:procedure)
.where("dossiers.created_at + (duree_conservation_dossiers_dans_ds * INTERVAL '1 month') - INTERVAL :expires_in < :now", { now: Time.zone.now, expires_in: INTERVAL_BEFORE_EXPIRATION })
end
scope :en_construction_close_to_expiration, -> do
state_en_construction
.joins(:procedure)
.where("dossiers.en_construction_at + dossiers.en_construction_conservation_extension + (duree_conservation_dossiers_dans_ds * INTERVAL '1 month') - INTERVAL :expires_in < :now", { now: Time.zone.now, expires_in: INTERVAL_BEFORE_EXPIRATION })
end
scope :en_instruction_close_to_expiration, -> do
state_en_instruction
.joins(:procedure)
.where("dossiers.en_instruction_at + (duree_conservation_dossiers_dans_ds * INTERVAL '1 month') - INTERVAL :expires_in < :now", { now: Time.zone.now, expires_in: INTERVAL_BEFORE_EXPIRATION })
end
scope :brouillon_expired, -> do
state_brouillon
.where("brouillon_close_to_expiration_notice_sent_at + INTERVAL :expires_in < :now", { now: Time.zone.now, expires_in: INTERVAL_EXPIRATION })
end
scope :en_construction_expired, -> do
state_en_construction
.where("en_construction_close_to_expiration_notice_sent_at + INTERVAL :expires_in < :now", { now: Time.zone.now, expires_in: INTERVAL_EXPIRATION })
end
scope :without_brouillon_expiration_notice_sent, -> { where(brouillon_close_to_expiration_notice_sent_at: nil) } scope :without_brouillon_expiration_notice_sent, -> { where(brouillon_close_to_expiration_notice_sent_at: nil) }
scope :without_en_construction_expiration_notice_sent, -> { where(en_construction_close_to_expiration_notice_sent_at: nil) } scope :without_en_construction_expiration_notice_sent, -> { where(en_construction_close_to_expiration_notice_sent_at: nil) }
scope :discarded_brouillon_expired, -> do
with_discarded
.discarded
.state_brouillon
.where('hidden_at < ?', 1.month.ago)
end
scope :discarded_en_construction_expired, -> do
with_discarded
.discarded
.state_en_construction
.joins(:procedure)
.where('dossiers.hidden_at < ?', 1.month.ago)
.where(procedures: { hidden_at: nil })
end
scope :brouillon_near_procedure_closing_date, -> do
# select users who have submitted dossier for the given 'procedures.id'
users_who_submitted =
state_not_brouillon
.joins(:groupe_instructeur)
.where("groupe_instructeurs.procedure_id = procedures.id")
.select(:user_id)
# select dossier in brouillon where procedure closes in two days and for which the user has not submitted a Dossier
brouillon.joins(:procedure)
.where("procedures.auto_archive_on - INTERVAL :before_closing = :now", { now: Time.zone.today, before_closing: INTERVAL_BEFORE_CLOSING })
.where.not(user: users_who_submitted)
end
scope :for_procedure, -> (procedure) { includes(:user, :groupe_instructeur).where(groupe_instructeurs: { procedure: procedure }) } scope :for_procedure, -> (procedure) { includes(:user, :groupe_instructeur).where(groupe_instructeurs: { procedure: procedure }) }
scope :for_api_v2, -> { includes(procedure: [:administrateurs], etablissement: [], individual: []) } scope :for_api_v2, -> { includes(procedure: [:administrateurs], etablissement: [], individual: []) }
@ -310,6 +354,10 @@ class Dossier < ApplicationRecord
instruction_commencee? && retention_end_date <= Time.zone.now instruction_commencee? && retention_end_date <= Time.zone.now
end end
def en_construction_close_to_expiration?
Dossier.en_construction_close_to_expiration.where(id: self).present?
end
def assign_to_groupe_instructeur(groupe_instructeur, author = nil) def assign_to_groupe_instructeur(groupe_instructeur, author = nil)
if groupe_instructeur.procedure == procedure && groupe_instructeur != self.groupe_instructeur if groupe_instructeur.procedure == procedure && groupe_instructeur != self.groupe_instructeur
if update(groupe_instructeur: groupe_instructeur, groupe_instructeur_updated_at: Time.zone.now) if update(groupe_instructeur: groupe_instructeur, groupe_instructeur_updated_at: Time.zone.now)
@ -368,6 +416,14 @@ class Dossier < ApplicationRecord
end end
end end
def log_operations?
!procedure.brouillon?
end
def keep_track_on_deletion?
!procedure.brouillon?
end
def expose_legacy_carto_api? def expose_legacy_carto_api?
procedure.expose_legacy_carto_api? procedure.expose_legacy_carto_api?
end end
@ -404,21 +460,29 @@ class Dossier < ApplicationRecord
end end
end end
def delete_and_keep_track(author) def expired_keep_track!
deleted_dossier = DeletedDossier.create_from_dossier(self) if keep_track_on_deletion?
discard! DeletedDossier.create_from_dossier(self, :expired)
log_automatic_dossier_operation(:supprimer, self)
end
end
def delete_and_keep_track!(author, reason)
if keep_track_on_deletion? && en_construction?
deleted_dossier = DeletedDossier.create_from_dossier(self, reason)
if en_construction?
administration_emails = followers_instructeurs.present? ? followers_instructeurs.map(&:email) : procedure.administrateurs.map(&:email) administration_emails = followers_instructeurs.present? ? followers_instructeurs.map(&:email) : procedure.administrateurs.map(&:email)
administration_emails.each do |email| administration_emails.each do |email|
DossierMailer.notify_deletion_to_administration(deleted_dossier, email).deliver_later DossierMailer.notify_deletion_to_administration(deleted_dossier, email).deliver_later
end end
end
DossierMailer.notify_deletion_to_user(deleted_dossier, user.email).deliver_later DossierMailer.notify_deletion_to_user(deleted_dossier, user.email).deliver_later
log_dossier_operation(author, :supprimer, self) log_dossier_operation(author, :supprimer, self)
end end
discard!
end
def after_passer_en_instruction(instructeur) def after_passer_en_instruction(instructeur)
instructeur.follow(self) instructeur.follow(self)
@ -624,6 +688,7 @@ class Dossier < ApplicationRecord
private private
def log_dossier_operation(author, operation, subject = nil) def log_dossier_operation(author, operation, subject = nil)
if log_operations?
DossierOperationLog.create_and_serialize( DossierOperationLog.create_and_serialize(
dossier: self, dossier: self,
operation: DossierOperationLog.operations.fetch(operation), operation: DossierOperationLog.operations.fetch(operation),
@ -631,8 +696,10 @@ class Dossier < ApplicationRecord
subject: subject subject: subject
) )
end end
end
def log_automatic_dossier_operation(operation, subject = nil) def log_automatic_dossier_operation(operation, subject = nil)
if log_operations?
DossierOperationLog.create_and_serialize( DossierOperationLog.create_and_serialize(
dossier: self, dossier: self,
operation: DossierOperationLog.operations.fetch(operation), operation: DossierOperationLog.operations.fetch(operation),
@ -640,6 +707,7 @@ class Dossier < ApplicationRecord
subject: subject subject: subject
) )
end end
end
def update_state_dates def update_state_dates
if en_construction? && !self.en_construction_at if en_construction? && !self.en_construction_at
@ -680,4 +748,12 @@ class Dossier < ApplicationRecord
end end
end end
end end
def self.notify_draft_not_submitted
brouillon_near_procedure_closing_date
.includes(:user)
.find_each do |dossier|
DossierMailer.notify_brouillon_not_submitted(dossier).deliver_later
end
end
end end

View file

@ -12,8 +12,9 @@ class DossierOperationLog < ApplicationRecord
demander_un_avis: 'demander_un_avis' demander_un_avis: 'demander_un_avis'
} }
belongs_to :dossier
has_one_attached :serialized has_one_attached :serialized
belongs_to :dossier, optional: true
belongs_to :bill_signature, optional: true belongs_to :bill_signature, optional: true
def self.create_and_serialize(params) def self.create_and_serialize(params)

View file

@ -111,7 +111,7 @@ class User < ApplicationRecord
end end
def can_be_deleted? def can_be_deleted?
administrateur.nil? && instructeur.nil? && dossiers.state_instruction_commencee.empty? administrateur.nil? && instructeur.nil? && dossiers.with_discarded.state_instruction_commencee.empty?
end end
def delete_and_keep_track_dossiers(administration) def delete_and_keep_track_dossiers(administration)
@ -120,7 +120,7 @@ class User < ApplicationRecord
end end
dossiers.each do |dossier| dossiers.each do |dossier|
dossier.delete_and_keep_track(administration) dossier.delete_and_keep_track!(administration, :user_removed)
end end
dossiers.with_discarded.destroy_all dossiers.with_discarded.destroy_all
destroy! destroy!

View file

@ -15,6 +15,7 @@ class ExpiredDossiersDeletionService
.without_brouillon_expiration_notice_sent .without_brouillon_expiration_notice_sent
dossiers_close_to_expiration dossiers_close_to_expiration
.with_notifiable_procedure
.includes(:user, :procedure) .includes(:user, :procedure)
.group_by(&:user) .group_by(&:user)
.each do |(user, dossiers)| .each do |(user, dossiers)|
@ -33,6 +34,7 @@ class ExpiredDossiersDeletionService
.without_en_construction_expiration_notice_sent .without_en_construction_expiration_notice_sent
dossiers_close_to_expiration dossiers_close_to_expiration
.with_notifiable_procedure
.includes(:user) .includes(:user)
.group_by(&:user) .group_by(&:user)
.each do |(user, dossiers)| .each do |(user, dossiers)|
@ -56,6 +58,7 @@ class ExpiredDossiersDeletionService
dossiers_to_remove = Dossier.brouillon_expired dossiers_to_remove = Dossier.brouillon_expired
dossiers_to_remove dossiers_to_remove
.with_notifiable_procedure
.includes(:user, :procedure) .includes(:user, :procedure)
.group_by(&:user) .group_by(&:user)
.each do |(user, dossiers)| .each do |(user, dossiers)|
@ -65,20 +68,15 @@ class ExpiredDossiersDeletionService
).deliver_later ).deliver_later
end end
dossiers_to_remove.each do |dossier| dossiers_to_remove.destroy_all
DeletedDossier.create_from_dossier(dossier)
dossier.destroy
end
end end
def self.delete_expired_en_construction_and_notify def self.delete_expired_en_construction_and_notify
dossiers_to_remove = Dossier.en_construction_expired dossiers_to_remove = Dossier.en_construction_expired
dossiers_to_remove.each(&:expired_keep_track!)
dossiers_to_remove.each do |dossier|
DeletedDossier.create_from_dossier(dossier)
end
dossiers_to_remove dossiers_to_remove
.with_notifiable_procedure
.includes(:user) .includes(:user)
.group_by(&:user) .group_by(&:user)
.each do |(user, dossiers)| .each do |(user, dossiers)|
@ -102,6 +100,7 @@ class ExpiredDossiersDeletionService
def self.group_by_fonctionnaire_email(dossiers) def self.group_by_fonctionnaire_email(dossiers)
dossiers dossiers
.with_notifiable_procedure
.includes(:followers_instructeurs, procedure: [:administrateurs]) .includes(:followers_instructeurs, procedure: [:administrateurs])
.each_with_object(Hash.new { |h, k| h[k] = Set.new }) do |dossier, h| .each_with_object(Hash.new { |h, k| h[k] = Set.new }) do |dossier, h|
(dossier.followers_instructeurs + dossier.procedure.administrateurs).each { |destinataire| h[destinataire.email] << dossier } (dossier.followers_instructeurs + dossier.procedure.administrateurs).each { |destinataire| h[destinataire.email] << dossier }

View file

@ -1,3 +1,3 @@
<%= render_flash(timeout: 5000, sticky: true) %> <%= render_flash(timeout: 5000, sticky: true) %>
<%= remove_element("#attachment_#{@attachment_id}") %> <%= remove_element(".attachment-actions-#{@attachment_id}") %>
<%= show_element("#attachment_file_#{@attachment_id}") %> <%= show_element(".attachment-input-#{@attachment_id}") %>

View file

@ -0,0 +1,24 @@
- content_for(:title, "#{@subject}")
%p
Bonjour,
%p
Le dossier n°#{@dossier.id} pour la démarche «&nbsp;
%strong
#{@dossier.procedure.libelle}
&nbsp;» est commencé mais n'est pas encore déposé.
%p
Si vous souhaitez que ce dossier soit pris en compte, il vous faut le déposer avant le
#{l(@dossier.procedure.auto_archive_on - 1.day, format: '%-d %B %Y')} à 23h59, date de cloture de la démarche.
%p
Pour cela, affichez le dossier avec le bouton ci-dessous, vérifiez votre dossier puis
cliquez sur le bouton
%strong
'Déposer le dossier'
%p
Si vous ne souhaitez plus déposer le dossier, vous n'avez rien à faire.
= round_button('Afficher votre dossier', dossier_url(@dossier), :primary)
= render partial: "layouts/mailers/signature"

View file

@ -0,0 +1,74 @@
- content_for(:title, "#{@procedure.libelle}")
#procedure-show
.sub-header
.container.flex
.procedure-logo{ style: "background-image: url(#{@procedure.logo_url})",
role: 'img', 'aria-label': "logo de la démarche #{@procedure.libelle}" }
.procedure-header
%h1= procedure_libelle @procedure
= link_to 'gestion des notifications', email_notifications_instructeur_procedure_path(@procedure), class: 'header-link'
|
= link_to 'statistiques', stats_instructeur_procedure_path(@procedure), class: 'header-link', data: { turbolinks: false } # Turbolinks disabled for Chartkick. See Issue #350
- if @procedure.routee?
|
- if current_administrateur.present? && current_administrateur.owns?(@procedure)
= link_to 'instructeurs', procedure_groupe_instructeurs_path(@procedure), class: 'header-link'
- else
= link_to 'instructeurs', instructeur_groupes_path(@procedure), class: 'header-link'
%ul.tabs
= tab_item('à suivre',
instructeur_procedure_path(@procedure, statut: 'a-suivre'))
= tab_item(t('pluralize.followed', count: 1),
instructeur_procedure_path(@procedure, statut: 'suivis'),
active: @statut == 'suivis')
= tab_item(t('pluralize.processed', count: 1),
instructeur_procedure_path(@procedure, statut: 'traites'))
= tab_item('tous les dossiers',
instructeur_procedure_path(@procedure, statut: 'tous'))
= tab_item(t('pluralize.archived', count: 1),
instructeur_procedure_path(@procedure, statut: 'archives'),
active: true)
.container
%h1.titre-dossiers Dossiers supprimés
%details.explication-onglet
%summary Les dossiers ont été supprimés. Vous ne pouvez plus les récupérer depuis Démarches Simplifiées.
Ceci s'explique pour les raisons suivantes :
%ul
%li L'utilisateur a intentionnellement supprimé son dossier.
%li Le délai de conservation maximal de #{@procedure.duree_conservation_dossiers_dans_ds} mois a expiré. Conformément au règlement RGPD, DS ne peut continuer à les héberger.
- if @deleted_dossiers.any?
= paginate @deleted_dossiers
%table.table.dossiers-table.hoverable
%thead
%tr
%th.notification-col
%th.number-col N° dossier
%th.status-col Etat
%th.status-col Raison de suppression
%th.status-col Date de suppression
%tbody
- @deleted_dossiers.each do |deleted_dossier|
%tr
%td.folder-col
%span.icon.folder
%td.number-col
= deleted_dossier.dossier_id
%td.status-col
= status_badge(deleted_dossier.state)
%td.reason-col
= deletion_reason_badge(deleted_dossier.reason)
%td.date-col.deleted-cell
= l(deleted_dossier.deleted_at, format: '%d/%m/%y')
= paginate @deleted_dossiers
- else
Aucun dossier supprimé

View file

@ -63,6 +63,10 @@
%p.explication-onglet Tous les dossiers qui ont été déposés sur cette démarche, sans aucun filtre. %p.explication-onglet Tous les dossiers qui ont été déposés sur cette démarche, sans aucun filtre.
- if @statut == 'archives' - if @statut == 'archives'
%p.explication-onglet Les dossiers de cet onglet sont archivés : vous ne pouvez plus y répondre, et les demandeurs ne peuvent plus les modifier. %p.explication-onglet Les dossiers de cet onglet sont archivés : vous ne pouvez plus y répondre, et les demandeurs ne peuvent plus les modifier.
.afficher-dossiers-supprimes
= link_to deleted_dossiers_instructeur_procedure_path(@procedure) do
%span.icon.delete
Afficher les dossiers supprimés
- if @dossiers.present? || @current_filters.count > 0 - if @dossiers.present? || @current_filters.count > 0
= paginate @dossiers = paginate @dossiers
@ -139,7 +143,7 @@
%td.status-col %td.status-col
= link_to(instructeur_dossier_path(@procedure, dossier), class: 'cell-link') do = link_to(instructeur_dossier_path(@procedure, dossier), class: 'cell-link') do
= render partial: 'shared/dossiers/status_badge', locals: { dossier: dossier } = status_badge(dossier.state)
%td.action-col.follow-col= render partial: 'dossier_actions', locals: { procedure: @procedure, dossier: dossier, dossier_is_followed: @followed_dossiers_id.include?(dossier.id) } %td.action-col.follow-col= render partial: 'dossier_actions', locals: { procedure: @procedure, dossier: dossier, dossier_is_followed: @followed_dossiers_id.include?(dossier.id) }
= paginate @dossiers = paginate @dossiers
- else - else

View file

@ -30,7 +30,7 @@
%td= link_to(dossier.user.email, dossier_linked_path(current_instructeur, dossier), class: 'cell-link') %td= link_to(dossier.user.email, dossier_linked_path(current_instructeur, dossier), class: 'cell-link')
%td.status-col %td.status-col
= link_to(dossier_linked_path(current_instructeur, dossier), class: 'cell-link') do = link_to(dossier_linked_path(current_instructeur, dossier), class: 'cell-link') do
= render partial: 'shared/dossiers/status_badge', locals: { dossier: dossier } = status_badge(dossier.state)
%td.action-col.follow-col= render partial: 'instructeurs/procedures/dossier_actions', locals: { procedure: dossier.procedure, dossier: dossier, dossier_is_followed: @followed_dossiers_id.include?(dossier.id) } %td.action-col.follow-col= render partial: 'instructeurs/procedures/dossier_actions', locals: { procedure: dossier.procedure, dossier: dossier, dossier_is_followed: @followed_dossiers_id.include?(dossier.id) }
- else - else
%h2 Aucun dossier correspondant à votre recherche n'a été trouvé %h2 Aucun dossier correspondant à votre recherche n'a été trouvé

View file

@ -15,8 +15,12 @@
= form_tag dossier_invites_path(dossier), remote: true, method: :post, class: 'form' do = form_tag dossier_invites_path(dossier), remote: true, method: :post, class: 'form' do
.row .row
.col .col
= email_field_tag :invite_email, '', class: 'small', placeholder: 'adresse email', required: true %span
= label_tag :invite_email, "Adresse email"
= email_field_tag :invite_email, '', class: 'small', placeholder: 'Adresse email', required: true
.col .col
%span
= label_tag :invite_message, "Ajouter un message à la personne invitée (optionnel)"
= text_area_tag :invite_message, '', class: 'small', placeholder: 'Ajouter un message à la personne invitée (optionnel)' = text_area_tag :invite_message, '', class: 'small', placeholder: 'Ajouter un message à la personne invitée (optionnel)'
.col .col
= submit_tag 'Envoyer une invitation', class: 'button accepted' = submit_tag 'Envoyer une invitation', class: 'button accepted'

View file

@ -1,5 +1,6 @@
.dropdown.header-menu-opener .dropdown.header-menu-opener
%button.button.dropdown-button.header-menu-button{ title: "Mon compte" } %button.button.dropdown-button.header-menu-button{ title: "Mon compte" }
.hidden Mon compte
= image_tag "icons/account-circle.svg", alt: '' = image_tag "icons/account-circle.svg", alt: ''
%ul.header-menu.dropdown-content %ul.header-menu.dropdown-content
%li %li

View file

@ -47,9 +47,10 @@
%li %li
.header-search{ role: 'search' } .header-search{ role: 'search' }
= form_tag recherche_dossiers_path, method: :post, class: "form" do = form_tag recherche_dossiers_path, method: :post, class: "form" do
= label_tag :dossier_id, "Numéro de dossier", class: 'hidden'
= text_field_tag :dossier_id, "", placeholder: "Numéro de dossier" = text_field_tag :dossier_id, "", placeholder: "Numéro de dossier"
%button{ title: "Rechercher" } %button{ title: "Rechercher" }
= image_tag "icons/search-blue.svg", alt: '' = image_tag "icons/search-blue.svg", alt: 'Rechercher', 'aria-hidden':'true'
- if instructeur_signed_in? || user_signed_in? - if instructeur_signed_in? || user_signed_in?
%li %li

View file

@ -1,4 +1,6 @@
- content_for(:title, 'Accessibilité') - content_for(:title, 'Accessibilité')
- content_for :footer do
= render partial: "root/footer"
.accessibilite .accessibilite

View file

@ -1,4 +1,6 @@
- content_for(:title, 'Suivi') - content_for(:title, 'Suivi')
- content_for :footer do
= render partial: "root/footer"
.suivi .suivi
%h1.new-h1 Cookies déposés et configuration du suivi %h1.new-h1 Cookies déposés et configuration du suivi

View file

@ -13,17 +13,16 @@
= link_to('le modèle suivant', url_for(template), target: '_blank', rel: 'noopener') = link_to('le modèle suivant', url_for(template), target: '_blank', rel: 'noopener')
- if persisted - if persisted
.attachment-actions{ id: "attachment_#{attachment_id}" } .attachment-actions{ class: "attachment-actions-#{attachment_id}" }
.attachment-action .attachment-action
= render partial: "shared/attachment/show", locals: { attachment: attachment, user_can_upload: true } = render partial: "shared/attachment/show", locals: { attachment: attachment, user_can_upload: true }
- if user_can_destroy - if user_can_destroy
.attachment-action .attachment-action
= link_to 'Supprimer', attachment_url(attachment.id, { signed_id: attachment.blob.signed_id }), remote: true, method: :delete, class: 'button small danger' = link_to 'Supprimer', attachment_url(attachment.id, { signed_id: attachment.blob.signed_id }), remote: true, method: :delete, class: 'button small danger'
.attachment-action .attachment-action
= button_tag 'Remplacer', type: 'button', class: 'button small', data: { 'toggle-target': "#attachment_file_#{attachment_id}" } = button_tag 'Remplacer', type: 'button', class: 'button small', data: { 'toggle-target': ".attachment-input-#{attachment_id}" }
= form.file_field attached_file.name, = form.file_field attached_file.name,
id: "attachment_file_#{attachment_id}", class: "attachment-input attachment-input-#{attachment_id} #{'hidden' if persisted}",
class: "attachment-input #{'hidden' if persisted}",
accept: accept, accept: accept,
direct_upload: true direct_upload: true

View file

@ -1,12 +0,0 @@
- if dossier.brouillon?
%span.label.brouillon brouillon
- elsif dossier.en_construction?
%span.label.construction en construction
- elsif dossier.en_instruction?
%span.label.instruction en instruction
- elsif dossier.accepte?
%span.label.accepted accepté
- elsif dossier.refuse?
%span.label.refused refusé
- elsif dossier.sans_suite?
%span.label.without-continuation sans suite

View file

@ -1,11 +1,10 @@
= # we do this trick because some html elements should use 'label' and some should be plain paragraphs
- if champ.html_label?
= form.label champ.main_value_name do = form.label champ.main_value_name do
#{champ.libelle} = render partial: 'shared/dossiers/editable_champs/champ_label_content', locals: { champ: champ, seen_at: seen_at }
- if champ.mandatory? - else
%span.mandatory * %h4.form-label
= render partial: 'shared/dossiers/editable_champs/champ_label_content', locals: { champ: champ, seen_at: seen_at }
- if champ.updated_at.present? && seen_at.present?
%span.updated-at{ class: highlight_if_unseen_class(seen_at, champ.updated_at) }
= "modifié le #{try_format_datetime(champ.updated_at)}"
- if champ.description.present? - if champ.description.present?
.notice{ id: describedby_id(champ) }= string_to_html(champ.description) .notice{ id: describedby_id(champ) }= string_to_html(champ.description)

View file

@ -0,0 +1,7 @@
#{champ.libelle}
- if champ.mandatory?
%span.mandatory *
- if champ.updated_at.present? && seen_at.present?
%span.updated-at{ class: highlight_if_unseen_class(seen_at, champ.updated_at) }
= "modifié le #{try_format_datetime(champ.updated_at)}"

View file

@ -1,4 +1,6 @@
.radios %fieldset.radios
%legend.mandatory-explanation
Sélectionnez une des valeurs
%label %label
= form.radio_button :value, Individual::GENDER_MALE = form.radio_button :value, Individual::GENDER_MALE
Monsieur Monsieur

View file

@ -3,6 +3,8 @@
champ.primary_options, champ.primary_options,
{ required: champ.mandatory? }, { required: champ.mandatory? },
{ data: { secondary_options: champ.secondary_options } } { data: { secondary_options: champ.secondary_options } }
%span
= form.label :secondary_value, "Valeur secondaire dépendant de la première", class: 'hidden'
= form.select :secondary_value, = form.select :secondary_value,
champ.secondary_options[champ.primary_value], champ.secondary_options[champ.primary_value],
{ required: champ.mandatory? }, { required: champ.mandatory? },

View file

@ -1,4 +1,6 @@
.radios %fieldset.radios
%legend.mandatory-explanation
Sélectionnez une des deux valeurs
%label %label
= form.radio_button :value, true = form.radio_button :value, true
Oui Oui

View file

@ -1,4 +1,6 @@
- content_for(:title, 'Statistiques') - content_for(:title, 'Statistiques')
- content_for :footer do
= render partial: "root/footer"
.statistiques .statistiques
-# Load Chartkick lazily, by using our React lazy-loader. -# Load Chartkick lazily, by using our React lazy-loader.

View file

@ -1,4 +1,6 @@
- content_for(:title, 'Contact') - content_for(:title, 'Contact')
- content_for :footer do
= render partial: "root/footer"
#contact-form #contact-form
.container .container

View file

@ -8,12 +8,20 @@
%p.mb-1 Merci de remplir vos informations personnelles pour accéder à la démarche. %p.mb-1 Merci de remplir vos informations personnelles pour accéder à la démarche.
%label %span.form-label
%span.mandatory * %span.mandatory *
champs requis champs requis
%fieldset
%legend
= f.label :gender, class: "required" = f.label :gender, class: "required"
= f.select :gender, [Individual::GENDER_MALE, Individual::GENDER_FEMALE], {}, class: "small" .radios
%label
= f.radio_button :gender, Individual::GENDER_MALE
= Individual::GENDER_MALE
%label
= f.radio_button :gender, Individual::GENDER_FEMALE
= Individual::GENDER_FEMALE
.flex .flex
.inline-champ .inline-champ

View file

@ -44,7 +44,7 @@
= procedure_libelle(dossier.procedure) = procedure_libelle(dossier.procedure)
%td.status-col %td.status-col
= link_to(url_for_dossier(dossier), class: 'cell-link') do = link_to(url_for_dossier(dossier), class: 'cell-link') do
= render partial: 'shared/dossiers/status_badge', locals: { dossier: dossier } = status_badge(dossier.state)
%td.updated-at-col %td.updated-at-col
= link_to(url_for_dossier(dossier), class: 'cell-link') do = link_to(url_for_dossier(dossier), class: 'cell-link') do
= try_format_date(dossier.updated_at) = try_format_date(dossier.updated_at)

View file

@ -1,6 +1,6 @@
.sub-header .sub-header
.container .container
= render partial: 'shared/dossiers/status_badge', locals: { dossier: dossier } = status_badge(dossier.state)
.title-container .title-container
%span.icon.folder %span.icon.folder
@ -22,6 +22,16 @@
%li %li
= link_to "Tout le dossier", dossier_path(dossier, format: :pdf), target: "_blank", rel: "noopener", class: "menu-item menu-link" = link_to "Tout le dossier", dossier_path(dossier, format: :pdf), target: "_blank", rel: "noopener", class: "menu-item menu-link"
- if dossier.en_construction_close_to_expiration?
.card.warning
.card-title Votre dossier va expirer
%p
Votre dossier a été déposé, mais va bientôt expirer. Cela signifie qu'il va bientôt être supprimé sans avoir été traité par ladministration.
Si vous souhaitez le conserver afin de poursuivre la démarche, vous pouvez le conserver
un mois de plus en cliquant sur le bouton ci-dessous.
%br
= button_to 'Repousser sa suppression', users_dossier_repousser_expiration_path(dossier), class: 'button secondary'
%ul.tabs %ul.tabs
= dynamic_tab_item('Résumé', dossier_path(dossier)) = dynamic_tab_item('Résumé', dossier_path(dossier))
= dynamic_tab_item('Demande', [demande_dossier_path(dossier), modifier_dossier_path(dossier)]) = dynamic_tab_item('Demande', [demande_dossier_path(dossier), modifier_dossier_path(dossier)])

View file

@ -20,7 +20,7 @@
Non Non
= f.label :password, "Mot de passe" = f.label :password, "Mot de passe"
= f.password_field :password, value: @user.password, placeholder: "8 caractères minimum" = f.password_field :password, value: @user.password, placeholder: "8 caractères minimum", 'aria-describedby':'8 caractères minimum'
= f.submit "Créer un compte", class: "button large primary expand" = f.submit "Créer un compte", class: "button large primary expand"

View file

@ -0,0 +1,49 @@
# We monkey patch the DateTimeSelector in order to add accessibility labels
# https://stackoverflow.com/a/47836699
module ActionView
module Helpers
class DateTimeSelector
# Given an ordering of datetime components, create the selection HTML
# and join them with their appropriate separators.
def build_selects_from_types(order)
select = ""
order.reverse_each do |type|
separator = separator(type)
select.insert(0, separator.to_s + send("select_#{type}").to_s)
end
# rubocop:disable Rails/OutputSafety
select.html_safe
# rubocop:enable Rails/OutputSafety
end
def datetime_accessibility_label(n, label)
prefix_re = @options[:prefix].match('(.*)\[(.*)\]\[(\d+)\]')
if prefix_re.nil? || prefix_re.size < 2
prefix = []
else
prefix = prefix_re.to_a.drop(1)
end
field_for = "#{prefix.join('_')}_#{@options[:field_name]}"
"<span class='hidden'><label for='#{field_for}_#{n}i'>#{label}</label></span>"
end
# Returns the separator for a given datetime component.
def separator(type)
return "" if @options[:use_hidden]
case type
when :year
datetime_accessibility_label(1, 'Année')
when :month
datetime_accessibility_label(2, 'Mois')
when :day
datetime_accessibility_label(3, 'Jour')
when :hour
(@options[:discard_year] && @options[:discard_day]) ? "" : @options[:datetime_separator] + datetime_accessibility_label(4, 'Heure')
when :minute, :second
@options[:"discard_#{type}"] ? "" : datetime_accessibility_label(5, 'Minute')
end
end
end
end
end

View file

@ -0,0 +1,94 @@
# frozen_string_literal: true
# from https://gist.github.com/Envek/7077bfc36b17233f60ad
# PostgreSQL interval data type support from https://github.com/rails/rails/pull/16919
# Works with both Rails 5.2 and 6.0
# Place this file to config/initializers/
require "active_support/duration"
# activerecord/lib/active_record/connection_adapters/postgresql/oid/interval.rb
module ActiveRecord
module ConnectionAdapters
module PostgreSQL
module OID # :nodoc:
class Interval < Type::Value # :nodoc:
def type
:interval
end
def cast_value(value)
case value
when ::ActiveSupport::Duration
value
when ::String
begin
::ActiveSupport::Duration.parse(value)
rescue ::ActiveSupport::Duration::ISO8601Parser::ParsingError
nil
end
else
super
end
end
def serialize(value)
case value
when ::ActiveSupport::Duration
value.iso8601(precision: self.precision)
when ::Numeric
# Sometimes operations on Times returns just float number of seconds so we need to handle that.
# Example: Time.current - (Time.current + 1.hour) # => -3600.000001776 (Float)
value.seconds.iso8601(precision: self.precision)
else
super
end
end
def type_cast_for_schema(value)
serialize(value).inspect
end
end
end
end
end
end
# activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
require 'active_record/connection_adapters/postgresql_adapter'
PostgreSQLAdapterWithInterval = Module.new do
def initialize_type_map(m = type_map)
super
m.register_type "interval" do |*_args, sql_type|
precision = extract_precision(sql_type)
::ActiveRecord::ConnectionAdapters::PostgreSQLAdapter::OID::Interval.new(precision: precision)
end
end
def configure_connection
super
execute('SET intervalstyle = iso_8601', 'SCHEMA')
end
ActiveRecord::Type.register(:interval, ::ActiveRecord::ConnectionAdapters::PostgreSQLAdapter::OID::Interval, adapter: :postgresql)
end
ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.prepend(PostgreSQLAdapterWithInterval)
# activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb
require 'active_record/connection_adapters/postgresql/schema_statements'
module SchemaStatementsWithInterval
def type_to_sql(type, limit: nil, precision: nil, scale: nil, array: nil, **)
case type.to_s
when 'interval'
case precision
when nil; "interval"
when 0..6; "interval(#{precision})"
else raise(ActiveRecordError, "No interval type has precision of #{precision}. The allowed range of precision is from 0 to 6")
end
else
super
end
end
end
ActiveRecord::ConnectionAdapters::PostgreSQL::SchemaStatements.prepend(SchemaStatementsWithInterval)

View file

@ -18,7 +18,7 @@ CADRE_JURIDIQUE_URL = [DOC_URL, "tutoriels/video-le-cadre-juridique"].join("/")
WEBINAIRE_URL = "https://app.livestorm.co/demarches-simplifiees" WEBINAIRE_URL = "https://app.livestorm.co/demarches-simplifiees"
LISTE_DES_DEMARCHES_URL = [DOC_URL, "listes-des-demarches"].join("/") LISTE_DES_DEMARCHES_URL = [DOC_URL, "listes-des-demarches"].join("/")
CGU_URL = [DOC_URL, "cgu"].join("/") CGU_URL = [DOC_URL, "cgu"].join("/")
MENTIONS_LEGALES_URL = [CGU_URL, "4-mentions-legales"].join("#") MENTIONS_LEGALES_URL = [DOC_URL, "mentions-legales"].join("/")
API_DOC_URL = [DOC_URL, "pour-aller-plus-loin", "api"].join("/") API_DOC_URL = [DOC_URL, "pour-aller-plus-loin", "api"].join("/")
WEBHOOK_DOC_URL = [DOC_URL, "pour-aller-plus-loin", "webhook"].join("/") WEBHOOK_DOC_URL = [DOC_URL, "pour-aller-plus-loin", "webhook"].join("/")
FAQ_URL = "https://faq.demarches-simplifiees.fr" FAQ_URL = "https://faq.demarches-simplifiees.fr"

View file

@ -1,5 +1,5 @@
fr: fr:
activerecord: activerecord:
attributes: attributes:
champs: champ:
value: La valeur du champ value: La valeur du champ

View file

@ -0,0 +1,11 @@
fr:
activerecord:
attributes:
deleted_dossier:
reason:
user_request: Demande dusager
manager_request: Demande dadministration
user_removed: Suppression d'un compte usager
expired: Expiration
unknown: Inconnue

View file

@ -146,6 +146,7 @@ Rails.application.routes.draw do
post '/carte/zones' => 'carte#zones' post '/carte/zones' => 'carte#zones'
get '/carte' => 'carte#show' get '/carte' => 'carte#show'
post '/carte' => 'carte#save' post '/carte' => 'carte#save'
post '/repousser-expiration' => 'dossiers#extend_conservation'
end end
# Redirection of legacy "/users/dossiers" route to "/dossiers" # Redirection of legacy "/users/dossiers" route to "/dossiers"
@ -306,6 +307,7 @@ Rails.application.routes.draw do
get 'stats' get 'stats'
get 'email_notifications' get 'email_notifications'
patch 'update_email_notifications' patch 'update_email_notifications'
get 'deleted_dossiers'
resources :dossiers, only: [:show], param: :dossier_id do resources :dossiers, only: [:show], param: :dossier_id do
member do member do

View file

@ -0,0 +1,5 @@
class AddEnConstructionConservationExtensionToDossiers < ActiveRecord::Migration[5.2]
def change
add_column :dossiers, :en_construction_conservation_extension, :interval, :default => 0.days
end
end

View file

@ -0,0 +1,5 @@
class AddReasonToDeletedDossiers < ActiveRecord::Migration[5.2]
def change
add_column :deleted_dossiers, :reason, :string
end
end

View file

@ -10,7 +10,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 2020_03_04_155418) do ActiveRecord::Schema.define(version: 2020_03_19_103836) do
# These are extensions that must be enabled in order to support this database # These are extensions that must be enabled in order to support this database
enable_extension "plpgsql" enable_extension "plpgsql"
@ -213,6 +213,7 @@ ActiveRecord::Schema.define(version: 2020_03_04_155418) do
t.string "state" t.string "state"
t.datetime "created_at", null: false t.datetime "created_at", null: false
t.datetime "updated_at", null: false t.datetime "updated_at", null: false
t.string "reason"
t.index ["procedure_id"], name: "index_deleted_dossiers_on_procedure_id" t.index ["procedure_id"], name: "index_deleted_dossiers_on_procedure_id"
end end
@ -254,6 +255,7 @@ ActiveRecord::Schema.define(version: 2020_03_04_155418) do
t.datetime "en_construction_close_to_expiration_notice_sent_at" t.datetime "en_construction_close_to_expiration_notice_sent_at"
t.index "to_tsvector('french'::regconfig, (search_terms || private_search_terms))", name: "index_dossiers_on_search_terms_private_search_terms", using: :gin t.index "to_tsvector('french'::regconfig, (search_terms || private_search_terms))", name: "index_dossiers_on_search_terms_private_search_terms", using: :gin
t.index "to_tsvector('french'::regconfig, search_terms)", name: "index_dossiers_on_search_terms", using: :gin t.index "to_tsvector('french'::regconfig, search_terms)", name: "index_dossiers_on_search_terms", using: :gin
t.interval "en_construction_conservation_extension", default: "00:00:00"
t.index ["archived"], name: "index_dossiers_on_archived" t.index ["archived"], name: "index_dossiers_on_archived"
t.index ["groupe_instructeur_id"], name: "index_dossiers_on_groupe_instructeur_id" t.index ["groupe_instructeur_id"], name: "index_dossiers_on_groupe_instructeur_id"
t.index ["hidden_at"], name: "index_dossiers_on_hidden_at" t.index ["hidden_at"], name: "index_dossiers_on_hidden_at"

View file

@ -411,6 +411,20 @@ describe Instructeurs::ProceduresController, type: :controller do
end end
end end
describe '#deleted_dossiers' do
let(:instructeur) { create(:instructeur) }
let(:procedure) { create(:procedure, instructeurs: [instructeur]) }
let(:deleted_dossier) { create(:deleted_dossier, procedure: procedure, state: :en_construction) }
let!(:deleted_dossier_brouillon) { create(:deleted_dossier, procedure: procedure, state: :brouillon) }
before do
sign_in(instructeur.user)
get :deleted_dossiers, params: { procedure_id: procedure.id }
end
it { expect(assigns(:deleted_dossiers)).to match_array([deleted_dossier]) }
end
describe '#update_email_notifications' do describe '#update_email_notifications' do
let(:instructeur) { create(:instructeur) } let(:instructeur) { create(:instructeur) }
let!(:procedure) { create(:procedure, instructeurs: [instructeur]) } let!(:procedure) { create(:procedure, instructeurs: [instructeur]) }

View file

@ -189,6 +189,7 @@ feature 'The routing', js: true do
visit commencer_path(path: procedure.reload.path) visit commencer_path(path: procedure.reload.path)
click_on 'Commencer la démarche' click_on 'Commencer la démarche'
choose 'M.'
fill_in 'individual_nom', with: 'Nom' fill_in 'individual_nom', with: 'Nom'
fill_in 'individual_prenom', with: 'Prenom' fill_in 'individual_prenom', with: 'Prenom'
click_button('Continuer') click_button('Continuer')

View file

@ -19,7 +19,7 @@ feature 'The user' do
fill_in('text', with: 'super texte') fill_in('text', with: 'super texte')
fill_in('textarea', with: 'super textarea') fill_in('textarea', with: 'super textarea')
fill_in('date', with: '12-12-2012') fill_in('date', with: '12-12-2012')
select_date_and_time(Time.zone.parse('06/01/1985 7h05'), form_id_for('datetime')) select_date_and_time(Time.zone.parse('06/01/1985 7h05'), form_id_for_datetime('datetime'))
fill_in('number', with: '42') fill_in('number', with: '42')
check('checkbox') check('checkbox')
choose('Madame') choose('Madame')
@ -74,7 +74,7 @@ feature 'The user' do
expect(page).to have_field('text', with: 'super texte') expect(page).to have_field('text', with: 'super texte')
expect(page).to have_field('textarea', with: 'super textarea') expect(page).to have_field('textarea', with: 'super textarea')
expect(page).to have_field('date', with: '2012-12-12') expect(page).to have_field('date', with: '2012-12-12')
check_date_and_time(Time.zone.parse('06/01/1985 7:05'), form_id_for('datetime')) check_date_and_time(Time.zone.parse('06/01/1985 7:05'), form_id_for_datetime('datetime'))
expect(page).to have_field('number', with: '42') expect(page).to have_field('number', with: '42')
expect(page).to have_checked_field('checkbox') expect(page).to have_checked_field('checkbox')
expect(page).to have_checked_field('Madame') expect(page).to have_checked_field('Madame')
@ -167,7 +167,7 @@ feature 'The user' do
fill_individual fill_individual
# Add an attachment # Add an attachment
find('.editable-champ-piece_justificative input[type=file]').attach_file(Rails.root + 'spec/fixtures/files/file.pdf') find_field('Pièce justificative').attach_file(Rails.root + 'spec/fixtures/files/file.pdf')
click_on 'Enregistrer le brouillon' click_on 'Enregistrer le brouillon'
expect(page).to have_content('Votre brouillon a bien été sauvegardé') expect(page).to have_content('Votre brouillon a bien été sauvegardé')
expect(page).to have_text('file.pdf') expect(page).to have_text('file.pdf')
@ -182,7 +182,7 @@ feature 'The user' do
# Replace the attachment # Replace the attachment
within('.attachment') { click_on 'Remplacer' } within('.attachment') { click_on 'Remplacer' }
find('.editable-champ-piece_justificative input[type=file]').attach_file(Rails.root + 'spec/fixtures/files/RIB.pdf') find_field('Pièce justificative').attach_file(Rails.root + 'spec/fixtures/files/RIB.pdf')
click_on 'Enregistrer le brouillon' click_on 'Enregistrer le brouillon'
expect(page).to have_no_text('file.pdf') expect(page).to have_no_text('file.pdf')
expect(page).to have_text('RIB.pdf') expect(page).to have_text('RIB.pdf')
@ -250,12 +250,39 @@ feature 'The user' do
find(:xpath, ".//label[contains(text()[normalize-space()], '#{libelle}')]")[:for] find(:xpath, ".//label[contains(text()[normalize-space()], '#{libelle}')]")[:for]
end end
def form_id_for_datetime(libelle)
# The HTML for datetime is a bit specific since it has 5 selects, below here is a sample HTML
# So, we want to find the partial id of a datetime (partial because there are 5 ids:
# dossier_champs_attributes_3_value_1i, 2i, ... 5i) ; we are interested in the 'dossier_champs_attributes_3_value' part
# which is then completed in select_date_and_time and check_date_and_time
#
# We find the H2, find the first select in the next .datetime div, then strip the last 3 characters
#
# <h4 class="form-label">
# libelle
# </h4>
# <div class="datetime">
# <span class="hidden">
# <label for="dossier_champs_attributes_3_value_3i">Jour</label></span>
# <select id="dossier_champs_attributes_3_value_3i" name="dossier[champs_attributes][3][value(3i)]">
# <option value=""></option>
# <option value="1">1</option>
# <option value="2">2</option>
# <!-- … -->
# </select>
# <!-- … 4 other selects for month, year, minute and seconds -->
# </div>
e = find(:xpath, ".//h4[contains(text()[normalize-space()], '#{libelle}')]")
e.sibling('.datetime').first('select')[:id][0..-4]
end
def champ_value_for(libelle) def champ_value_for(libelle)
champs = user_dossier.champs champs = user_dossier.champs
champs.find { |c| c.libelle == libelle }.value champs.find { |c| c.libelle == libelle }.value
end end
def fill_individual def fill_individual
choose 'M.'
fill_in('individual_prenom', with: 'prenom') fill_in('individual_prenom', with: 'prenom')
fill_in('individual_nom', with: 'nom') fill_in('individual_nom', with: 'nom')
click_on 'Continuer' click_on 'Continuer'

View file

@ -22,6 +22,7 @@ feature 'Creating a new dossier:' do
expect(page).to have_current_path identite_dossier_path(user.reload.dossiers.last) expect(page).to have_current_path identite_dossier_path(user.reload.dossiers.last)
expect(page).to have_procedure_description(procedure) expect(page).to have_procedure_description(procedure)
choose 'M.'
fill_in 'individual_nom', with: 'Nom' fill_in 'individual_nom', with: 'Nom'
fill_in 'individual_prenom', with: 'Prenom' fill_in 'individual_prenom', with: 'Prenom'
end end

View file

@ -62,6 +62,7 @@ feature 'linked dropdown lists' do
end end
def fill_individual def fill_individual
choose 'M.'
fill_in('individual_prenom', with: 'prenom') fill_in('individual_prenom', with: 'prenom')
fill_in('individual_nom', with: 'nom') fill_in('individual_nom', with: 'nom')
click_on 'Continuer' click_on 'Continuer'

View file

@ -5,7 +5,7 @@ RSpec.describe AutoReceiveDossiersForProcedureJob, type: :job do
let(:date) { Time.utc(2017, 9, 1, 10, 5, 0) } let(:date) { Time.utc(2017, 9, 1, 10, 5, 0) }
let(:instruction_date) { date + 120 } let(:instruction_date) { date + 120 }
let(:procedure) { create(:procedure, :with_instructeur) } let(:procedure) { create(:procedure, :published, :with_instructeur) }
let(:nouveau_dossier1) { create(:dossier, :en_construction, procedure: procedure) } let(:nouveau_dossier1) { create(:dossier, :en_construction, procedure: procedure) }
let(:nouveau_dossier2) { create(:dossier, :en_construction, procedure: procedure) } let(:nouveau_dossier2) { create(:dossier, :en_construction, procedure: procedure) }
let(:dossier_recu) { create(:dossier, :en_instruction, procedure: procedure) } let(:dossier_recu) { create(:dossier, :en_instruction, procedure: procedure) }

View file

@ -6,7 +6,7 @@ RSpec.describe DeclarativeProceduresJob, type: :job do
let(:instruction_date) { date + 120 } let(:instruction_date) { date + 120 }
let(:state) { nil } let(:state) { nil }
let(:procedure) { create(:procedure, :with_instructeur, declarative_with_state: state) } let(:procedure) { create(:procedure, :published, :with_instructeur, declarative_with_state: state) }
let(:nouveau_dossier1) { create(:dossier, :en_construction, procedure: procedure) } let(:nouveau_dossier1) { create(:dossier, :en_construction, procedure: procedure) }
let(:nouveau_dossier2) { create(:dossier, :en_construction, procedure: procedure) } let(:nouveau_dossier2) { create(:dossier, :en_construction, procedure: procedure) }
let(:dossier_recu) { create(:dossier, :en_instruction, procedure: procedure) } let(:dossier_recu) { create(:dossier, :en_instruction, procedure: procedure) }

View file

@ -98,7 +98,7 @@ RSpec.describe DossierMailer, type: :mailer do
describe '.notify_automatic_deletion_to_user' do describe '.notify_automatic_deletion_to_user' do
let(:dossier) { create(:dossier) } let(:dossier) { create(:dossier) }
let(:deleted_dossier) { DeletedDossier.create_from_dossier(dossier) } let(:deleted_dossier) { DeletedDossier.create_from_dossier(dossier, :expired) }
before do before do
duree = dossier.procedure.duree_conservation_dossiers_dans_ds duree = dossier.procedure.duree_conservation_dossiers_dans_ds
@ -116,7 +116,7 @@ RSpec.describe DossierMailer, type: :mailer do
describe '.notify_automatic_deletion_to_administration' do describe '.notify_automatic_deletion_to_administration' do
let(:dossier) { create(:dossier) } let(:dossier) { create(:dossier) }
let(:deleted_dossier) { DeletedDossier.create_from_dossier(dossier) } let(:deleted_dossier) { DeletedDossier.create_from_dossier(dossier, :expired) }
subject { described_class.notify_automatic_deletion_to_administration([deleted_dossier], dossier.user.email) } subject { described_class.notify_automatic_deletion_to_administration([deleted_dossier], dossier.user.email) }

View file

@ -53,6 +53,10 @@ class DossierMailerPreview < ActionMailer::Preview
DossierMailer.notify_automatic_deletion_to_administration([deleted_dossier, deleted_dossier], administration_email) DossierMailer.notify_automatic_deletion_to_administration([deleted_dossier, deleted_dossier], administration_email)
end end
def notify_brouillon_not_submitted
DossierMailer.notify_brouillon_not_submitted(draft)
end
private private
def usager_email def usager_email
@ -76,7 +80,7 @@ class DossierMailerPreview < ActionMailer::Preview
end end
def procedure def procedure
Procedure.new(id: 1234, libelle: 'Dotation dÉquipement des Territoires Ruraux - Exercice 2019', service: service, logo: Rack::Test::UploadedFile.new("./spec/fixtures/files/logo_test_procedure.png", 'image/png')) Procedure.new(id: 1234, libelle: 'Dotation dÉquipement des Territoires Ruraux - Exercice 2019', service: service, logo: Rack::Test::UploadedFile.new("./spec/fixtures/files/logo_test_procedure.png", 'image/png'), auto_archive_on: Time.zone.today + Dossier::REMAINING_DAYS_BEFORE_CLOSING.days)
end end
def service def service

View file

@ -46,7 +46,7 @@ describe Dossier do
end end
describe 'brouillon_close_to_expiration' do describe 'brouillon_close_to_expiration' do
let(:procedure) { create(:procedure, duree_conservation_dossiers_dans_ds: 6) } let(:procedure) { create(:procedure, :published, duree_conservation_dossiers_dans_ds: 6) }
let!(:young_dossier) { create(:dossier, :en_construction, procedure: procedure) } let!(:young_dossier) { create(:dossier, :en_construction, procedure: procedure) }
let!(:expiring_dossier) { create(:dossier, created_at: 170.days.ago, procedure: procedure) } let!(:expiring_dossier) { create(:dossier, created_at: 170.days.ago, procedure: procedure) }
let!(:just_expired_dossier) { create(:dossier, created_at: (6.months + 1.hour + 10.seconds).ago, procedure: procedure) } let!(:just_expired_dossier) { create(:dossier, created_at: (6.months + 1.hour + 10.seconds).ago, procedure: procedure) }
@ -63,7 +63,7 @@ describe Dossier do
end end
describe 'en_construction_close_to_expiration' do describe 'en_construction_close_to_expiration' do
let(:procedure) { create(:procedure, duree_conservation_dossiers_dans_ds: 6) } let(:procedure) { create(:procedure, :published, duree_conservation_dossiers_dans_ds: 6) }
let!(:young_dossier) { create(:dossier, procedure: procedure) } let!(:young_dossier) { create(:dossier, procedure: procedure) }
let!(:expiring_dossier) { create(:dossier, :en_construction, en_construction_at: 170.days.ago, procedure: procedure) } let!(:expiring_dossier) { create(:dossier, :en_construction, en_construction_at: 170.days.ago, procedure: procedure) }
let!(:just_expired_dossier) { create(:dossier, :en_construction, en_construction_at: (6.months + 1.hour + 10.seconds).ago, procedure: procedure) } let!(:just_expired_dossier) { create(:dossier, :en_construction, en_construction_at: (6.months + 1.hour + 10.seconds).ago, procedure: procedure) }
@ -77,10 +77,19 @@ describe Dossier do
is_expected.to include(just_expired_dossier) is_expected.to include(just_expired_dossier)
is_expected.to include(long_expired_dossier) is_expected.to include(long_expired_dossier)
end end
context 'does not include an expiring dossier that has been postponed' do
before do
expiring_dossier.update(en_construction_conservation_extension: 1.month)
expiring_dossier.reload
end
it { is_expected.not_to include(expiring_dossier) }
end
end end
describe 'en_instruction_close_to_expiration' do describe 'en_instruction_close_to_expiration' do
let(:procedure) { create(:procedure, duree_conservation_dossiers_dans_ds: 6) } let(:procedure) { create(:procedure, :published, duree_conservation_dossiers_dans_ds: 6) }
let!(:young_dossier) { create(:dossier, procedure: procedure) } let!(:young_dossier) { create(:dossier, procedure: procedure) }
let!(:expiring_dossier) { create(:dossier, :en_instruction, en_instruction_at: 170.days.ago, procedure: procedure) } let!(:expiring_dossier) { create(:dossier, :en_instruction, en_instruction_at: 170.days.ago, procedure: procedure) }
let!(:just_expired_dossier) { create(:dossier, :en_instruction, en_instruction_at: (6.months + 1.hour + 10.seconds).ago, procedure: procedure) } let!(:just_expired_dossier) { create(:dossier, :en_instruction, en_instruction_at: (6.months + 1.hour + 10.seconds).ago, procedure: procedure) }
@ -441,7 +450,7 @@ describe Dossier do
end end
describe "#unfollow_stale_instructeurs" do describe "#unfollow_stale_instructeurs" do
let(:procedure) { create(:procedure) } let(:procedure) { create(:procedure, :published) }
let(:instructeur) { create(:instructeur) } let(:instructeur) { create(:instructeur) }
let(:new_groupe_instructeur) { create(:groupe_instructeur, procedure: procedure) } let(:new_groupe_instructeur) { create(:groupe_instructeur, procedure: procedure) }
let(:instructeur2) { create(:instructeur, groupe_instructeurs: [procedure.defaut_groupe_instructeur, new_groupe_instructeur]) } let(:instructeur2) { create(:instructeur, groupe_instructeurs: [procedure.defaut_groupe_instructeur, new_groupe_instructeur]) }
@ -661,23 +670,42 @@ describe Dossier do
end end
end end
describe "#delete_and_keep_track" do describe "#delete_and_keep_track!" do
let(:dossier) { create(:dossier) } let(:dossier) { create(:dossier, :en_construction) }
let(:deleted_dossier) { DeletedDossier.find_by!(dossier_id: dossier.id) } let(:deleted_dossier) { DeletedDossier.find_by(dossier_id: dossier.id) }
let(:last_operation) { dossier.dossier_operation_logs.last } let(:last_operation) { dossier.dossier_operation_logs.last }
let(:reason) { :user_request }
before do before do
allow(DossierMailer).to receive(:notify_deletion_to_user).and_return(double(deliver_later: nil)) allow(DossierMailer).to receive(:notify_deletion_to_user).and_return(double(deliver_later: nil))
allow(DossierMailer).to receive(:notify_deletion_to_administration).and_return(double(deliver_later: nil)) allow(DossierMailer).to receive(:notify_deletion_to_administration).and_return(double(deliver_later: nil))
end end
subject! { dossier.delete_and_keep_track(dossier.user) } subject! { dossier.delete_and_keep_track!(dossier.user, reason) }
context 'brouillon' do
let(:dossier) { create(:dossier) }
it 'hides the dossier' do
expect(dossier.discarded?).to be_truthy
end
it 'do not creates a DeletedDossier record' do
expect(deleted_dossier).to be_nil
end
it 'do not records the operation in the log' do
expect(last_operation).to be_nil
end
end
context 'en_construction' do
it 'hides the dossier' do it 'hides the dossier' do
expect(dossier.hidden_at).to be_present expect(dossier.hidden_at).to be_present
end end
it 'creates a DeletedDossier record' do it 'creates a DeletedDossier record' do
expect(deleted_dossier.reason).to eq DeletedDossier.reasons.fetch(reason)
expect(deleted_dossier.dossier_id).to eq dossier.id expect(deleted_dossier.dossier_id).to eq dossier.id
expect(deleted_dossier.procedure).to eq dossier.procedure expect(deleted_dossier.procedure).to eq dossier.procedure
expect(deleted_dossier.state).to eq dossier.state expect(deleted_dossier.state).to eq dossier.state
@ -721,6 +749,33 @@ describe Dossier do
expect(DossierMailer).not_to have_received(:notify_deletion_to_administration) expect(DossierMailer).not_to have_received(:notify_deletion_to_administration)
end end
end end
context 'with reason: manager_request' do
let(:reason) { :manager_request }
it 'hides the dossier' do
expect(dossier.discarded?).to be_truthy
end
it 'records the operation in the log' do
expect(last_operation.operation).to eq("supprimer")
expect(last_operation.automatic_operation?).to be_falsey
end
end
context 'with reason: user_removed' do
let(:reason) { :user_removed }
it 'hides the dossier' do
expect(dossier.discarded?).to be_truthy
end
it 'records the operation in the log' do
expect(last_operation.operation).to eq("supprimer")
expect(last_operation.automatic_operation?).to be_falsey
end
end
end
end end
describe 'webhook' do describe 'webhook' do
@ -1097,6 +1152,33 @@ describe Dossier do
it { expect(Dossier.for_procedure(procedure_2)).to contain_exactly(dossier_2_1) } it { expect(Dossier.for_procedure(procedure_2)).to contain_exactly(dossier_2_1) }
end end
describe '#notify_draft_not_submitted' do
let!(:user1) { create(:user) }
let!(:user2) { create(:user) }
let!(:procedure_near_closing) { create(:procedure, auto_archive_on: Time.zone.today + Dossier::REMAINING_DAYS_BEFORE_CLOSING.days) }
let!(:procedure_closed_later) { create(:procedure, auto_archive_on: Time.zone.today + Dossier::REMAINING_DAYS_BEFORE_CLOSING.days + 1.day) }
let!(:procedure_closed_before) { create(:procedure, auto_archive_on: Time.zone.today + Dossier::REMAINING_DAYS_BEFORE_CLOSING.days - 1.day) }
# user 1 has three draft dossiers where one is for procedure that closes in two days ==> should trigger one mail
let!(:draft_near_closing) { create(:dossier, user: user1, procedure: procedure_near_closing) }
let!(:draft_before) { create(:dossier, user: user1, procedure: procedure_closed_before) }
let!(:draft_later) { create(:dossier, user: user1, procedure: procedure_closed_later) }
# user 2 submitted a draft and en_construction dossier for the same procedure ==> should not trigger the mail
let!(:draft_near_closing_2) { create(:dossier, :en_construction, user: user2, procedure: procedure_near_closing) }
let!(:submitted_near_closing_2) { create(:dossier, user: user2, procedure: procedure_near_closing) }
before do
allow(DossierMailer).to receive(:notify_brouillon_not_submitted).and_return(double(deliver_later: nil))
Dossier.notify_draft_not_submitted
end
it 'notifies draft is not submitted' do
expect(DossierMailer).to have_received(:notify_brouillon_not_submitted).once
expect(DossierMailer).to have_received(:notify_brouillon_not_submitted).with(draft_near_closing)
end
end
describe '#geo_position' do describe '#geo_position' do
let(:lat) { "46.538192" } let(:lat) { "46.538192" }
let(:lon) { "2.428462" } let(:lon) { "2.428462" }
@ -1131,4 +1213,41 @@ describe Dossier do
end end
end end
end end
describe 'dossier_operation_log after dossier deletion' do
let(:dossier) { create(:dossier) }
let(:dossier_operation_log) { create(:dossier_operation_log, dossier: dossier) }
it 'should nullify dossier link' do
expect(dossier_operation_log.dossier).to eq(dossier)
expect(DossierOperationLog.count).to eq(1)
dossier.destroy
expect(dossier_operation_log.reload.dossier).to be_nil
expect(DossierOperationLog.count).to eq(1)
end
end
describe 'discarded_brouillon_expired and discarded_en_construction_expired' do
before do
create(:dossier)
create(:dossier, :en_construction)
create(:dossier).discard!
create(:dossier, :en_construction).discard!
Timecop.travel(2.months.ago) do
create(:dossier).discard!
create(:dossier, :en_construction).discard!
create(:dossier).procedure.hide!
create(:dossier, :en_construction).procedure.hide!
end
Timecop.travel(1.week.ago) do
create(:dossier).discard!
create(:dossier, :en_construction).discard!
end
end
it { expect(Dossier.discarded_brouillon_expired.count).to eq(2) }
it { expect(Dossier.discarded_en_construction_expired.count).to eq(1) }
end
end end

View file

@ -264,7 +264,7 @@ describe User, type: :model do
user.delete_and_keep_track_dossiers(administration) user.delete_and_keep_track_dossiers(administration)
expect(DeletedDossier.find_by(dossier_id: dossier_en_construction)).to be_present expect(DeletedDossier.find_by(dossier_id: dossier_en_construction)).to be_present
expect(DeletedDossier.find_by(dossier_id: dossier_brouillon)).to be_present expect(DeletedDossier.find_by(dossier_id: dossier_brouillon)).to be_nil
expect(User.find_by(id: user.id)).to be_nil expect(User.find_by(id: user.id)).to be_nil
end end
end end
@ -278,16 +278,16 @@ describe User, type: :model do
end end
it "keep track of dossiers and delete user" do it "keep track of dossiers and delete user" do
dossier_cache.delete_and_keep_track(administration) dossier_cache.delete_and_keep_track!(administration, :user_request)
user.delete_and_keep_track_dossiers(administration) user.delete_and_keep_track_dossiers(administration)
expect(DeletedDossier.find_by(dossier_id: dossier_en_construction)).to be_present expect(DeletedDossier.find_by(dossier_id: dossier_en_construction)).to be_present
expect(DeletedDossier.find_by(dossier_id: dossier_brouillon)).to be_present expect(DeletedDossier.find_by(dossier_id: dossier_brouillon)).to be_nil
expect(User.find_by(id: user.id)).to be_nil expect(User.find_by(id: user.id)).to be_nil
end end
it "doesn't destroy dossiers of another user" do it "doesn't destroy dossiers of another user" do
dossier_cache.delete_and_keep_track(administration) dossier_cache.delete_and_keep_track!(administration, :user_request)
user.delete_and_keep_track_dossiers(administration) user.delete_and_keep_track_dossiers(administration)
expect(Dossier.find_by(id: dossier_from_another_user.id)).to be_present expect(Dossier.find_by(id: dossier_from_another_user.id)).to be_present

View file

@ -4,7 +4,7 @@ describe ExpiredDossiersDeletionService do
describe '#process_expired_dossiers_brouillon' do describe '#process_expired_dossiers_brouillon' do
let(:draft_expiration) { 1.month + 5.days } let(:draft_expiration) { 1.month + 5.days }
let!(:today) { Time.zone.now.at_midnight } let!(:today) { Time.zone.now.at_midnight }
let!(:procedure) { create(:procedure, duree_conservation_dossiers_dans_ds: 6) } let!(:procedure) { create(:procedure, :published, duree_conservation_dossiers_dans_ds: 6) }
let!(:date_close_to_expiration) { Date.today - procedure.duree_conservation_dossiers_dans_ds.months + 1.month } let!(:date_close_to_expiration) { Date.today - procedure.duree_conservation_dossiers_dans_ds.months + 1.month }
let!(:date_expired) { Date.today - procedure.duree_conservation_dossiers_dans_ds.months - 6.days } let!(:date_expired) { Date.today - procedure.duree_conservation_dossiers_dans_ds.months - 6.days }
let!(:date_not_expired) { Date.today - procedure.duree_conservation_dossiers_dans_ds.months + 2.months } let!(:date_not_expired) { Date.today - procedure.duree_conservation_dossiers_dans_ds.months + 2.months }
@ -34,7 +34,7 @@ describe ExpiredDossiersDeletionService do
it 'deletes and notify expired brouillon' do it 'deletes and notify expired brouillon' do
expect(DossierMailer).to have_received(:notify_brouillon_deletion).once expect(DossierMailer).to have_received(:notify_brouillon_deletion).once
expect(DossierMailer).to have_received(:notify_brouillon_deletion).with([expired_brouillon.hash_for_deletion_mail], expired_brouillon.user.email) expect(DossierMailer).to have_received(:notify_brouillon_deletion).with([expired_brouillon.hash_for_deletion_mail], expired_brouillon.user.email)
expect(DeletedDossier.find_by(dossier_id: expired_brouillon.id)).to be_present expect(DeletedDossier.find_by(dossier_id: expired_brouillon.id)).not_to be_present
expect { expired_brouillon.reload }.to raise_error(ActiveRecord::RecordNotFound) expect { expired_brouillon.reload }.to raise_error(ActiveRecord::RecordNotFound)
end end
end end
@ -192,7 +192,7 @@ describe ExpiredDossiersDeletionService do
end end
context 'when an instructeur is also administrateur' do context 'when an instructeur is also administrateur' do
let!(:procedure) { create(:procedure) } let!(:procedure) { create(:procedure, :published) }
let!(:administrateur) { procedure.administrateurs.first } let!(:administrateur) { procedure.administrateurs.first }
let!(:dossier) { create(:dossier, :en_construction, procedure: procedure, en_construction_at: (conservation_par_defaut - 1.month + 1.day).ago) } let!(:dossier) { create(:dossier, :en_construction, procedure: procedure, en_construction_at: (conservation_par_defaut - 1.month + 1.day).ago) }