commit
1aa4936b94
75 changed files with 854 additions and 185 deletions
11
Gemfile.lock
11
Gemfile.lock
|
@ -78,7 +78,7 @@ GEM
|
|||
tzinfo (~> 1.1)
|
||||
addressable (2.7.0)
|
||||
public_suffix (>= 2.0.2, < 5.0)
|
||||
administrate (0.12.0)
|
||||
administrate (0.13.0)
|
||||
actionpack (>= 4.2)
|
||||
actionview (>= 4.2)
|
||||
activerecord (>= 4.2)
|
||||
|
@ -212,7 +212,7 @@ GEM
|
|||
activesupport (>= 3.0.0)
|
||||
faraday (0.15.4)
|
||||
multipart-post (>= 1.2, < 3)
|
||||
ffi (1.9.25)
|
||||
ffi (1.12.2)
|
||||
flipper (0.17.2)
|
||||
flipper-active_record (0.17.2)
|
||||
activerecord (>= 4.2, < 7)
|
||||
|
@ -588,10 +588,9 @@ GEM
|
|||
sass-listen (4.0.0)
|
||||
rb-fsevent (~> 0.9, >= 0.9.4)
|
||||
rb-inotify (~> 0.9, >= 0.9.7)
|
||||
sassc (2.0.0)
|
||||
ffi (~> 1.9.6)
|
||||
rake
|
||||
sassc-rails (2.1.0)
|
||||
sassc (2.2.1)
|
||||
ffi (~> 1.9)
|
||||
sassc-rails (2.1.2)
|
||||
railties (>= 4.0.0)
|
||||
sassc (>= 2.0)
|
||||
sprockets (> 3.0)
|
||||
|
|
|
@ -68,17 +68,19 @@ En local, un utilisateur de test est créé automatiquement, avec les identifian
|
|||
### Programmation des jobs
|
||||
|
||||
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
|
||||
UpdateAdministrateurUsageStatisticsJob.set(cron: "0 10 * * *").perform_later
|
||||
FindDubiousProceduresJob.set(cron: "0 0 * * *").perform_later
|
||||
Administrateurs::ActivateBeforeExpirationJob.set(cron: "0 8 * * *").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
|
||||
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
|
||||
NotifyDraftNotSubmittedJob.set(cron: "0 7 * * *").perform_later
|
||||
DiscardedDossiersDeletionJob.set(cron: "0 7 * * *").perform_later
|
||||
|
||||
### Voir les emails envoyés en local
|
||||
|
||||
|
|
|
@ -32,6 +32,10 @@
|
|||
display: block;
|
||||
}
|
||||
|
||||
.deleted-cell {
|
||||
padding: (2 * $default-spacer) $default-spacer;
|
||||
}
|
||||
|
||||
.icon.folder {
|
||||
position: relative;
|
||||
|
||||
|
|
|
@ -44,6 +44,12 @@
|
|||
}
|
||||
}
|
||||
|
||||
.form-label {
|
||||
font-weight: bold;
|
||||
font-size: 18px;
|
||||
margin-bottom: $default-padding;
|
||||
}
|
||||
|
||||
.notice {
|
||||
@include notice-text-style;
|
||||
margin-top: - $default-spacer;
|
||||
|
|
|
@ -26,7 +26,7 @@
|
|||
|
||||
input[type=email] {
|
||||
width: auto;
|
||||
margin-bottom: 0;
|
||||
margin-bottom: $default-spacer;
|
||||
}
|
||||
|
||||
.button {
|
||||
|
|
|
@ -17,21 +17,41 @@
|
|||
border: 1px solid $blue;
|
||||
}
|
||||
|
||||
&.en-instruction {
|
||||
@extend .instruction;
|
||||
}
|
||||
|
||||
&.construction {
|
||||
background-color: #FFFFFF;
|
||||
color: $black;
|
||||
border: 1px solid $black;
|
||||
}
|
||||
|
||||
&.en-construction {
|
||||
@extend .construction;
|
||||
}
|
||||
|
||||
&.accepted {
|
||||
background-color: $green;
|
||||
}
|
||||
|
||||
&.accepte {
|
||||
@extend .accepted;
|
||||
}
|
||||
|
||||
&.refused {
|
||||
background-color: $dark-red;
|
||||
}
|
||||
|
||||
&.refuse {
|
||||
@extend .refused;
|
||||
}
|
||||
|
||||
&.without-continuation {
|
||||
background-color: $black;
|
||||
}
|
||||
|
||||
&.sans-suite {
|
||||
@extend .without-continuation;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
@import "common";
|
||||
@import "constants";
|
||||
@import "mixins";
|
||||
@import "utils";
|
||||
|
||||
$header-landing-breakpoint: 1040px;
|
||||
$header-mobile-breakpoint: 550px;
|
||||
|
@ -148,6 +149,10 @@ $header-mobile-breakpoint: 550px;
|
|||
margin: 0;
|
||||
}
|
||||
|
||||
label.hidden {
|
||||
@extend .hidden;
|
||||
}
|
||||
|
||||
button {
|
||||
@extend %outline;
|
||||
|
||||
|
|
|
@ -16,6 +16,10 @@
|
|||
margin-bottom: 1 * $default-padding;
|
||||
}
|
||||
|
||||
.titre-dossiers {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.dossiers-table {
|
||||
margin-top: $default-spacer;
|
||||
margin-bottom: 3 * $default-spacer;
|
||||
|
@ -30,6 +34,11 @@
|
|||
}
|
||||
}
|
||||
|
||||
.afficher-dossiers-supprimes {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.filter {
|
||||
display: inline-block;
|
||||
padding-left: 10px;
|
||||
|
@ -48,7 +57,7 @@
|
|||
display: inline-block;
|
||||
}
|
||||
|
||||
p.explication-onglet {
|
||||
.explication-onglet {
|
||||
margin-bottom: 3 * $default-spacer;
|
||||
text-align: center;
|
||||
}
|
||||
|
|
|
@ -113,6 +113,14 @@ module Instructeurs
|
|||
assign_exports
|
||||
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
|
||||
values = params[:values]
|
||||
|
||||
|
|
|
@ -22,7 +22,7 @@ module Manager
|
|||
|
||||
def hide
|
||||
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}")
|
||||
flash[:notice] = "Le dossier #{dossier.id} a été supprimé."
|
||||
|
|
|
@ -166,6 +166,12 @@ module Users
|
|||
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
|
||||
@dossier = dossier_with_champs
|
||||
end
|
||||
|
@ -203,7 +209,7 @@ module Users
|
|||
dossier = current_user.dossiers.includes(:user, procedure: :administrateurs).find(params[:id])
|
||||
|
||||
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é.'
|
||||
redirect_to dossiers_path
|
||||
else
|
||||
|
|
|
@ -12,7 +12,6 @@ class ProcedureDashboard < Administrate::BaseDashboard
|
|||
types_de_champ_private: TypesDeChampCollectionField,
|
||||
path: ProcedureLinkField,
|
||||
dossiers: Field::HasMany,
|
||||
instructeurs: Field::HasMany,
|
||||
administrateurs: Field::HasMany,
|
||||
id: Field::Number.with_options(searchable: true),
|
||||
libelle: Field::String,
|
||||
|
@ -75,7 +74,6 @@ class ProcedureDashboard < Administrate::BaseDashboard
|
|||
:types_de_champ_private,
|
||||
:for_individual,
|
||||
:auto_archive_on,
|
||||
:instructeurs,
|
||||
:initiated_mail_template,
|
||||
:received_mail_template,
|
||||
:closed_mail_template,
|
||||
|
|
|
@ -83,6 +83,24 @@ module DossierHelper
|
|||
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
|
||||
|
||||
def dinum_instance?
|
||||
|
|
|
@ -2,7 +2,7 @@ import { fire, timeoutable } from '@utils';
|
|||
|
||||
// Manages a queue of Autosave operations,
|
||||
// and sends `autosave:*` events to indicate the state of the requests.
|
||||
export default class AutosaveController {
|
||||
export default class AutoSaveController {
|
||||
constructor() {
|
||||
this.timeoutDelay = 60000; // 1mn
|
||||
this.latestPromise = Promise.resolve();
|
|
@ -1,4 +1,4 @@
|
|||
import AutosaveController from './autosave-controller.js';
|
||||
import AutoSaveController from './auto-save-controller.js';
|
||||
import {
|
||||
debounce,
|
||||
delegate,
|
||||
|
@ -14,7 +14,7 @@ const AUTOSAVE_DEBOUNCE_DELAY = gon.autosave.debounce_delay;
|
|||
const AUTOSAVE_STATUS_VISIBLE_DURATION = gon.autosave.status_visible_duration;
|
||||
|
||||
// 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.
|
||||
|
||||
|
@ -26,13 +26,13 @@ delegate(
|
|||
formInputsSelector,
|
||||
debounce(() => {
|
||||
const form = document.querySelector(formSelector);
|
||||
autosaveController.enqueueAutosaveRequest(form);
|
||||
autoSaveController.enqueueAutosaveRequest(form);
|
||||
}, AUTOSAVE_DEBOUNCE_DELAY)
|
||||
);
|
||||
|
||||
delegate('click', '.autosave-retry', () => {
|
||||
const form = document.querySelector(formSelector);
|
||||
autosaveController.enqueueAutosaveRequest(form);
|
||||
autoSaveController.enqueueAutosaveRequest(form);
|
||||
});
|
||||
|
||||
// Display some UI during the autosave
|
|
@ -16,13 +16,13 @@ import '../shared/franceconnect';
|
|||
import '../shared/toggle-target';
|
||||
|
||||
import '../new_design/dropdown';
|
||||
import '../new_design/autosave';
|
||||
import '../new_design/form-validation';
|
||||
import '../new_design/procedure-context';
|
||||
import '../new_design/procedure-form';
|
||||
import '../new_design/select2';
|
||||
import '../new_design/spinner';
|
||||
import '../new_design/support';
|
||||
import '../new_design/dossiers/auto-save';
|
||||
|
||||
import '../new_design/champs/carte';
|
||||
import '../new_design/champs/linked-drop-down-list';
|
||||
|
|
8
app/jobs/discarded_dossiers_deletion_job.rb
Normal file
8
app/jobs/discarded_dossiers_deletion_job.rb
Normal 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
|
7
app/jobs/notify_draft_not_submitted_job.rb
Normal file
7
app/jobs/notify_draft_not_submitted_job.rb
Normal file
|
@ -0,0 +1,7 @@
|
|||
class NotifyDraftNotSubmittedJob < ApplicationJob
|
||||
queue_as :cron
|
||||
|
||||
def perform(*args)
|
||||
Dossier.notify_draft_not_submitted
|
||||
end
|
||||
end
|
|
@ -104,4 +104,11 @@ class DossierMailer < ApplicationMailer
|
|||
|
||||
mail(from: NO_REPLY_EMAIL, to: instructeur.email, subject: @subject)
|
||||
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
|
||||
|
|
|
@ -18,7 +18,7 @@ class Administrateur < ApplicationRecord
|
|||
end
|
||||
|
||||
def email
|
||||
user.email
|
||||
user&.email
|
||||
end
|
||||
|
||||
# validate :password_complexity, if: Proc.new { |a| Devise.password_length.include?(a.password.try(:size)) }
|
||||
|
|
|
@ -80,6 +80,10 @@ class Champ < ApplicationRecord
|
|||
type_de_champ.to_typed_id
|
||||
end
|
||||
|
||||
def html_label?
|
||||
true
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def needs_dossier_id?
|
||||
|
|
|
@ -1,2 +1,5 @@
|
|||
class Champs::CiviliteChamp < Champ
|
||||
def html_label?
|
||||
false
|
||||
end
|
||||
end
|
||||
|
|
|
@ -13,6 +13,10 @@ class Champs::DatetimeChamp < Champ
|
|||
value.present? ? I18n.l(Time.zone.parse(value)) : ""
|
||||
end
|
||||
|
||||
def html_label?
|
||||
false
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def format_before_save
|
||||
|
|
|
@ -2,8 +2,8 @@ class Champs::DecimalNumberChamp < Champ
|
|||
validates :value, numericality: {
|
||||
allow_nil: true,
|
||||
allow_blank: true,
|
||||
message: -> (object, data) {
|
||||
"« #{object.libelle} » " + object.errors.generate_message(data[:attribute].downcase, :not_a_number)
|
||||
message: -> (object, _data) {
|
||||
"« #{object.libelle} » " + object.errors.generate_message(:value, :not_a_number)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -3,8 +3,8 @@ class Champs::IntegerNumberChamp < Champ
|
|||
only_integer: true,
|
||||
allow_nil: true,
|
||||
allow_blank: true,
|
||||
message: -> (object, data) {
|
||||
"« #{object.libelle} » " + object.errors.generate_message(data[:attribute].downcase, :not_an_integer)
|
||||
message: -> (object, _data) {
|
||||
"« #{object.libelle} » " + object.errors.generate_message(:value, :not_an_integer)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -16,6 +16,10 @@ class Champs::PieceJustificativeChamp < Champ
|
|||
"image/jpeg"
|
||||
]
|
||||
|
||||
def main_value_name
|
||||
:piece_justificative_file
|
||||
end
|
||||
|
||||
def search_terms
|
||||
# We don’t know how to search inside documents yet
|
||||
end
|
||||
|
|
|
@ -1,7 +1,20 @@
|
|||
class DeletedDossier < ApplicationRecord
|
||||
belongs_to :procedure
|
||||
|
||||
def self.create_from_dossier(dossier)
|
||||
DeletedDossier.create!(dossier_id: dossier.id, procedure: dossier.procedure, state: dossier.state, deleted_at: Time.zone.now)
|
||||
enum reason: {
|
||||
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
|
||||
|
|
|
@ -22,6 +22,11 @@ class Dossier < ApplicationRecord
|
|||
|
||||
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 :individual, validate: false, 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 :avis, inverse_of: :dossier, dependent: :destroy
|
||||
|
||||
has_many :dossier_operation_logs, dependent: :destroy
|
||||
has_many :dossier_operation_logs, dependent: :nullify
|
||||
|
||||
belongs_to :groupe_instructeur
|
||||
has_one :procedure, through: :groupe_instructeur
|
||||
|
@ -164,28 +169,67 @@ class Dossier < ApplicationRecord
|
|||
user: [])
|
||||
}
|
||||
|
||||
scope :brouillon_close_to_expiration, -> do
|
||||
brouillon
|
||||
.joins(:procedure)
|
||||
.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()")
|
||||
scope :with_notifiable_procedure, -> do
|
||||
joins(:procedure)
|
||||
.where.not(procedures: { aasm_state: :brouillon })
|
||||
end
|
||||
|
||||
scope :brouillon_expired, -> { brouillon.where("brouillon_close_to_expiration_notice_sent_at < (now() - INTERVAL '1 month 5 days')") }
|
||||
scope :en_construction_expired, -> { en_construction.where("en_construction_close_to_expiration_notice_sent_at < (now() - INTERVAL '1 month 5 days')") }
|
||||
scope :brouillon_close_to_expiration, -> do
|
||||
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_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_api_v2, -> { includes(procedure: [:administrateurs], etablissement: [], individual: []) }
|
||||
|
||||
|
@ -310,6 +354,10 @@ class Dossier < ApplicationRecord
|
|||
instruction_commencee? && retention_end_date <= Time.zone.now
|
||||
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)
|
||||
if groupe_instructeur.procedure == procedure && groupe_instructeur != self.groupe_instructeur
|
||||
if update(groupe_instructeur: groupe_instructeur, groupe_instructeur_updated_at: Time.zone.now)
|
||||
|
@ -368,6 +416,14 @@ class Dossier < ApplicationRecord
|
|||
end
|
||||
end
|
||||
|
||||
def log_operations?
|
||||
!procedure.brouillon?
|
||||
end
|
||||
|
||||
def keep_track_on_deletion?
|
||||
!procedure.brouillon?
|
||||
end
|
||||
|
||||
def expose_legacy_carto_api?
|
||||
procedure.expose_legacy_carto_api?
|
||||
end
|
||||
|
@ -404,19 +460,27 @@ class Dossier < ApplicationRecord
|
|||
end
|
||||
end
|
||||
|
||||
def delete_and_keep_track(author)
|
||||
deleted_dossier = DeletedDossier.create_from_dossier(self)
|
||||
discard!
|
||||
def expired_keep_track!
|
||||
if keep_track_on_deletion?
|
||||
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.each do |email|
|
||||
DossierMailer.notify_deletion_to_administration(deleted_dossier, email).deliver_later
|
||||
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
|
||||
|
||||
discard!
|
||||
end
|
||||
|
||||
def after_passer_en_instruction(instructeur)
|
||||
|
@ -624,21 +688,25 @@ class Dossier < ApplicationRecord
|
|||
private
|
||||
|
||||
def log_dossier_operation(author, operation, subject = nil)
|
||||
DossierOperationLog.create_and_serialize(
|
||||
dossier: self,
|
||||
operation: DossierOperationLog.operations.fetch(operation),
|
||||
author: author,
|
||||
subject: subject
|
||||
)
|
||||
if log_operations?
|
||||
DossierOperationLog.create_and_serialize(
|
||||
dossier: self,
|
||||
operation: DossierOperationLog.operations.fetch(operation),
|
||||
author: author,
|
||||
subject: subject
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
def log_automatic_dossier_operation(operation, subject = nil)
|
||||
DossierOperationLog.create_and_serialize(
|
||||
dossier: self,
|
||||
operation: DossierOperationLog.operations.fetch(operation),
|
||||
automatic_operation: true,
|
||||
subject: subject
|
||||
)
|
||||
if log_operations?
|
||||
DossierOperationLog.create_and_serialize(
|
||||
dossier: self,
|
||||
operation: DossierOperationLog.operations.fetch(operation),
|
||||
automatic_operation: true,
|
||||
subject: subject
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
def update_state_dates
|
||||
|
@ -680,4 +748,12 @@ class Dossier < ApplicationRecord
|
|||
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
|
||||
|
|
|
@ -12,8 +12,9 @@ class DossierOperationLog < ApplicationRecord
|
|||
demander_un_avis: 'demander_un_avis'
|
||||
}
|
||||
|
||||
belongs_to :dossier
|
||||
has_one_attached :serialized
|
||||
|
||||
belongs_to :dossier, optional: true
|
||||
belongs_to :bill_signature, optional: true
|
||||
|
||||
def self.create_and_serialize(params)
|
||||
|
|
|
@ -111,7 +111,7 @@ class User < ApplicationRecord
|
|||
end
|
||||
|
||||
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
|
||||
|
||||
def delete_and_keep_track_dossiers(administration)
|
||||
|
@ -120,7 +120,7 @@ class User < ApplicationRecord
|
|||
end
|
||||
|
||||
dossiers.each do |dossier|
|
||||
dossier.delete_and_keep_track(administration)
|
||||
dossier.delete_and_keep_track!(administration, :user_removed)
|
||||
end
|
||||
dossiers.with_discarded.destroy_all
|
||||
destroy!
|
||||
|
|
|
@ -15,6 +15,7 @@ class ExpiredDossiersDeletionService
|
|||
.without_brouillon_expiration_notice_sent
|
||||
|
||||
dossiers_close_to_expiration
|
||||
.with_notifiable_procedure
|
||||
.includes(:user, :procedure)
|
||||
.group_by(&:user)
|
||||
.each do |(user, dossiers)|
|
||||
|
@ -33,6 +34,7 @@ class ExpiredDossiersDeletionService
|
|||
.without_en_construction_expiration_notice_sent
|
||||
|
||||
dossiers_close_to_expiration
|
||||
.with_notifiable_procedure
|
||||
.includes(:user)
|
||||
.group_by(&:user)
|
||||
.each do |(user, dossiers)|
|
||||
|
@ -56,6 +58,7 @@ class ExpiredDossiersDeletionService
|
|||
dossiers_to_remove = Dossier.brouillon_expired
|
||||
|
||||
dossiers_to_remove
|
||||
.with_notifiable_procedure
|
||||
.includes(:user, :procedure)
|
||||
.group_by(&:user)
|
||||
.each do |(user, dossiers)|
|
||||
|
@ -65,20 +68,15 @@ class ExpiredDossiersDeletionService
|
|||
).deliver_later
|
||||
end
|
||||
|
||||
dossiers_to_remove.each do |dossier|
|
||||
DeletedDossier.create_from_dossier(dossier)
|
||||
dossier.destroy
|
||||
end
|
||||
dossiers_to_remove.destroy_all
|
||||
end
|
||||
|
||||
def self.delete_expired_en_construction_and_notify
|
||||
dossiers_to_remove = Dossier.en_construction_expired
|
||||
|
||||
dossiers_to_remove.each do |dossier|
|
||||
DeletedDossier.create_from_dossier(dossier)
|
||||
end
|
||||
dossiers_to_remove.each(&:expired_keep_track!)
|
||||
|
||||
dossiers_to_remove
|
||||
.with_notifiable_procedure
|
||||
.includes(:user)
|
||||
.group_by(&:user)
|
||||
.each do |(user, dossiers)|
|
||||
|
@ -102,6 +100,7 @@ class ExpiredDossiersDeletionService
|
|||
|
||||
def self.group_by_fonctionnaire_email(dossiers)
|
||||
dossiers
|
||||
.with_notifiable_procedure
|
||||
.includes(:followers_instructeurs, procedure: [:administrateurs])
|
||||
.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 }
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
<%= render_flash(timeout: 5000, sticky: true) %>
|
||||
<%= remove_element("#attachment_#{@attachment_id}") %>
|
||||
<%= show_element("#attachment_file_#{@attachment_id}") %>
|
||||
<%= remove_element(".attachment-actions-#{@attachment_id}") %>
|
||||
<%= show_element(".attachment-input-#{@attachment_id}") %>
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
- content_for(:title, "#{@subject}")
|
||||
|
||||
%p
|
||||
Bonjour,
|
||||
|
||||
%p
|
||||
Le dossier n°#{@dossier.id} pour la démarche «
|
||||
%strong
|
||||
#{@dossier.procedure.libelle}
|
||||
» 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"
|
74
app/views/instructeurs/procedures/deleted_dossiers.html.haml
Normal file
74
app/views/instructeurs/procedures/deleted_dossiers.html.haml
Normal 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é
|
||||
|
|
@ -63,6 +63,10 @@
|
|||
%p.explication-onglet Tous les dossiers qui ont été déposés sur cette démarche, sans aucun filtre.
|
||||
- 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.
|
||||
.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
|
||||
= paginate @dossiers
|
||||
|
@ -139,7 +143,7 @@
|
|||
|
||||
%td.status-col
|
||||
= 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) }
|
||||
= paginate @dossiers
|
||||
- else
|
||||
|
|
|
@ -30,7 +30,7 @@
|
|||
%td= link_to(dossier.user.email, dossier_linked_path(current_instructeur, dossier), class: 'cell-link')
|
||||
%td.status-col
|
||||
= 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) }
|
||||
- else
|
||||
%h2 Aucun dossier correspondant à votre recherche n'a été trouvé
|
||||
|
|
|
@ -15,8 +15,12 @@
|
|||
= form_tag dossier_invites_path(dossier), remote: true, method: :post, class: 'form' do
|
||||
.row
|
||||
.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
|
||||
%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)'
|
||||
.col
|
||||
= submit_tag 'Envoyer une invitation', class: 'button accepted'
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
.dropdown.header-menu-opener
|
||||
%button.button.dropdown-button.header-menu-button{ title: "Mon compte" }
|
||||
.hidden Mon compte
|
||||
= image_tag "icons/account-circle.svg", alt: ''
|
||||
%ul.header-menu.dropdown-content
|
||||
%li
|
||||
|
|
|
@ -47,9 +47,10 @@
|
|||
%li
|
||||
.header-search{ role: 'search' }
|
||||
= 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"
|
||||
%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?
|
||||
%li
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
- content_for(:title, 'Accessibilité')
|
||||
- content_for :footer do
|
||||
= render partial: "root/footer"
|
||||
|
||||
.accessibilite
|
||||
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
- content_for(:title, 'Suivi')
|
||||
- content_for :footer do
|
||||
= render partial: "root/footer"
|
||||
|
||||
.suivi
|
||||
%h1.new-h1 Cookies déposés et configuration du suivi
|
||||
|
|
|
@ -13,17 +13,16 @@
|
|||
= link_to('le modèle suivant', url_for(template), target: '_blank', rel: 'noopener')
|
||||
|
||||
- if persisted
|
||||
.attachment-actions{ id: "attachment_#{attachment_id}" }
|
||||
.attachment-actions{ class: "attachment-actions-#{attachment_id}" }
|
||||
.attachment-action
|
||||
= render partial: "shared/attachment/show", locals: { attachment: attachment, user_can_upload: true }
|
||||
- if user_can_destroy
|
||||
.attachment-action
|
||||
= link_to 'Supprimer', attachment_url(attachment.id, { signed_id: attachment.blob.signed_id }), remote: true, method: :delete, class: 'button small danger'
|
||||
.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,
|
||||
id: "attachment_file_#{attachment_id}",
|
||||
class: "attachment-input #{'hidden' if persisted}",
|
||||
class: "attachment-input attachment-input-#{attachment_id} #{'hidden' if persisted}",
|
||||
accept: accept,
|
||||
direct_upload: true
|
||||
|
|
|
@ -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
|
|
@ -1,11 +1,10 @@
|
|||
= form.label champ.main_value_name do
|
||||
#{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)}"
|
||||
= # 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
|
||||
= render partial: 'shared/dossiers/editable_champs/champ_label_content', locals: { champ: champ, seen_at: seen_at }
|
||||
- else
|
||||
%h4.form-label
|
||||
= render partial: 'shared/dossiers/editable_champs/champ_label_content', locals: { champ: champ, seen_at: seen_at }
|
||||
|
||||
- if champ.description.present?
|
||||
.notice{ id: describedby_id(champ) }= string_to_html(champ.description)
|
||||
|
|
|
@ -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)}"
|
|
@ -1,4 +1,6 @@
|
|||
.radios
|
||||
%fieldset.radios
|
||||
%legend.mandatory-explanation
|
||||
Sélectionnez une des valeurs
|
||||
%label
|
||||
= form.radio_button :value, Individual::GENDER_MALE
|
||||
Monsieur
|
||||
|
|
|
@ -3,6 +3,8 @@
|
|||
champ.primary_options,
|
||||
{ required: champ.mandatory? },
|
||||
{ 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,
|
||||
champ.secondary_options[champ.primary_value],
|
||||
{ required: champ.mandatory? },
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
.radios
|
||||
%fieldset.radios
|
||||
%legend.mandatory-explanation
|
||||
Sélectionnez une des deux valeurs
|
||||
%label
|
||||
= form.radio_button :value, true
|
||||
Oui
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
- content_for(:title, 'Statistiques')
|
||||
- content_for :footer do
|
||||
= render partial: "root/footer"
|
||||
|
||||
.statistiques
|
||||
-# Load Chartkick lazily, by using our React lazy-loader.
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
- content_for(:title, 'Contact')
|
||||
- content_for :footer do
|
||||
= render partial: "root/footer"
|
||||
|
||||
#contact-form
|
||||
.container
|
||||
|
|
|
@ -8,12 +8,20 @@
|
|||
|
||||
%p.mb-1 Merci de remplir vos informations personnelles pour accéder à la démarche.
|
||||
|
||||
%label
|
||||
%span.form-label
|
||||
%span.mandatory *
|
||||
champs requis
|
||||
|
||||
= f.label :gender, class: "required"
|
||||
= f.select :gender, [Individual::GENDER_MALE, Individual::GENDER_FEMALE], {}, class: "small"
|
||||
%fieldset
|
||||
%legend
|
||||
= f.label :gender, class: "required"
|
||||
.radios
|
||||
%label
|
||||
= f.radio_button :gender, Individual::GENDER_MALE
|
||||
= Individual::GENDER_MALE
|
||||
%label
|
||||
= f.radio_button :gender, Individual::GENDER_FEMALE
|
||||
= Individual::GENDER_FEMALE
|
||||
|
||||
.flex
|
||||
.inline-champ
|
||||
|
|
|
@ -44,7 +44,7 @@
|
|||
= procedure_libelle(dossier.procedure)
|
||||
%td.status-col
|
||||
= 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
|
||||
= link_to(url_for_dossier(dossier), class: 'cell-link') do
|
||||
= try_format_date(dossier.updated_at)
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
.sub-header
|
||||
.container
|
||||
= render partial: 'shared/dossiers/status_badge', locals: { dossier: dossier }
|
||||
= status_badge(dossier.state)
|
||||
|
||||
.title-container
|
||||
%span.icon.folder
|
||||
|
@ -22,6 +22,16 @@
|
|||
%li
|
||||
= 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 l’administration.
|
||||
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
|
||||
= dynamic_tab_item('Résumé', dossier_path(dossier))
|
||||
= dynamic_tab_item('Demande', [demande_dossier_path(dossier), modifier_dossier_path(dossier)])
|
||||
|
|
|
@ -20,7 +20,7 @@
|
|||
Non
|
||||
|
||||
= 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"
|
||||
|
||||
|
|
49
config/initializers/date_select.rb
Normal file
49
config/initializers/date_select.rb
Normal 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
|
94
config/initializers/pg_interval_5_2.rb
Normal file
94
config/initializers/pg_interval_5_2.rb
Normal 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)
|
|
@ -18,7 +18,7 @@ CADRE_JURIDIQUE_URL = [DOC_URL, "tutoriels/video-le-cadre-juridique"].join("/")
|
|||
WEBINAIRE_URL = "https://app.livestorm.co/demarches-simplifiees"
|
||||
LISTE_DES_DEMARCHES_URL = [DOC_URL, "listes-des-demarches"].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("/")
|
||||
WEBHOOK_DOC_URL = [DOC_URL, "pour-aller-plus-loin", "webhook"].join("/")
|
||||
FAQ_URL = "https://faq.demarches-simplifiees.fr"
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
fr:
|
||||
activerecord:
|
||||
attributes:
|
||||
champs:
|
||||
champ:
|
||||
value: La valeur du champ
|
||||
|
|
11
config/locales/models/deleted_dossier/fr.yml
Normal file
11
config/locales/models/deleted_dossier/fr.yml
Normal file
|
@ -0,0 +1,11 @@
|
|||
fr:
|
||||
activerecord:
|
||||
attributes:
|
||||
deleted_dossier:
|
||||
reason:
|
||||
user_request: Demande d’usager
|
||||
manager_request: Demande d’administration
|
||||
user_removed: Suppression d'un compte usager
|
||||
expired: Expiration
|
||||
unknown: Inconnue
|
||||
|
|
@ -146,6 +146,7 @@ Rails.application.routes.draw do
|
|||
post '/carte/zones' => 'carte#zones'
|
||||
get '/carte' => 'carte#show'
|
||||
post '/carte' => 'carte#save'
|
||||
post '/repousser-expiration' => 'dossiers#extend_conservation'
|
||||
end
|
||||
|
||||
# Redirection of legacy "/users/dossiers" route to "/dossiers"
|
||||
|
@ -306,6 +307,7 @@ Rails.application.routes.draw do
|
|||
get 'stats'
|
||||
get 'email_notifications'
|
||||
patch 'update_email_notifications'
|
||||
get 'deleted_dossiers'
|
||||
|
||||
resources :dossiers, only: [:show], param: :dossier_id do
|
||||
member do
|
||||
|
|
|
@ -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
|
|
@ -0,0 +1,5 @@
|
|||
class AddReasonToDeletedDossiers < ActiveRecord::Migration[5.2]
|
||||
def change
|
||||
add_column :deleted_dossiers, :reason, :string
|
||||
end
|
||||
end
|
|
@ -10,7 +10,7 @@
|
|||
#
|
||||
# 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
|
||||
enable_extension "plpgsql"
|
||||
|
@ -213,6 +213,7 @@ ActiveRecord::Schema.define(version: 2020_03_04_155418) do
|
|||
t.string "state"
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
t.string "reason"
|
||||
t.index ["procedure_id"], name: "index_deleted_dossiers_on_procedure_id"
|
||||
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.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.interval "en_construction_conservation_extension", default: "00:00:00"
|
||||
t.index ["archived"], name: "index_dossiers_on_archived"
|
||||
t.index ["groupe_instructeur_id"], name: "index_dossiers_on_groupe_instructeur_id"
|
||||
t.index ["hidden_at"], name: "index_dossiers_on_hidden_at"
|
||||
|
|
|
@ -411,6 +411,20 @@ describe Instructeurs::ProceduresController, type: :controller do
|
|||
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
|
||||
let(:instructeur) { create(:instructeur) }
|
||||
let!(:procedure) { create(:procedure, instructeurs: [instructeur]) }
|
||||
|
|
|
@ -189,6 +189,7 @@ feature 'The routing', js: true do
|
|||
visit commencer_path(path: procedure.reload.path)
|
||||
click_on 'Commencer la démarche'
|
||||
|
||||
choose 'M.'
|
||||
fill_in 'individual_nom', with: 'Nom'
|
||||
fill_in 'individual_prenom', with: 'Prenom'
|
||||
click_button('Continuer')
|
||||
|
|
|
@ -19,7 +19,7 @@ feature 'The user' do
|
|||
fill_in('text', with: 'super texte')
|
||||
fill_in('textarea', with: 'super textarea')
|
||||
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')
|
||||
check('checkbox')
|
||||
choose('Madame')
|
||||
|
@ -74,7 +74,7 @@ feature 'The user' do
|
|||
expect(page).to have_field('text', with: 'super texte')
|
||||
expect(page).to have_field('textarea', with: 'super textarea')
|
||||
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_checked_field('checkbox')
|
||||
expect(page).to have_checked_field('Madame')
|
||||
|
@ -167,7 +167,7 @@ feature 'The user' do
|
|||
fill_individual
|
||||
|
||||
# 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'
|
||||
expect(page).to have_content('Votre brouillon a bien été sauvegardé')
|
||||
expect(page).to have_text('file.pdf')
|
||||
|
@ -182,7 +182,7 @@ feature 'The user' do
|
|||
|
||||
# Replace the attachment
|
||||
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'
|
||||
expect(page).to have_no_text('file.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]
|
||||
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)
|
||||
champs = user_dossier.champs
|
||||
champs.find { |c| c.libelle == libelle }.value
|
||||
end
|
||||
|
||||
def fill_individual
|
||||
choose 'M.'
|
||||
fill_in('individual_prenom', with: 'prenom')
|
||||
fill_in('individual_nom', with: 'nom')
|
||||
click_on 'Continuer'
|
||||
|
|
|
@ -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_procedure_description(procedure)
|
||||
|
||||
choose 'M.'
|
||||
fill_in 'individual_nom', with: 'Nom'
|
||||
fill_in 'individual_prenom', with: 'Prenom'
|
||||
end
|
||||
|
|
|
@ -62,6 +62,7 @@ feature 'linked dropdown lists' do
|
|||
end
|
||||
|
||||
def fill_individual
|
||||
choose 'M.'
|
||||
fill_in('individual_prenom', with: 'prenom')
|
||||
fill_in('individual_nom', with: 'nom')
|
||||
click_on 'Continuer'
|
||||
|
|
|
@ -5,7 +5,7 @@ RSpec.describe AutoReceiveDossiersForProcedureJob, type: :job do
|
|||
let(:date) { Time.utc(2017, 9, 1, 10, 5, 0) }
|
||||
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_dossier2) { create(:dossier, :en_construction, procedure: procedure) }
|
||||
let(:dossier_recu) { create(:dossier, :en_instruction, procedure: procedure) }
|
||||
|
|
|
@ -6,7 +6,7 @@ RSpec.describe DeclarativeProceduresJob, type: :job do
|
|||
let(:instruction_date) { date + 120 }
|
||||
|
||||
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_dossier2) { create(:dossier, :en_construction, procedure: procedure) }
|
||||
let(:dossier_recu) { create(:dossier, :en_instruction, procedure: procedure) }
|
||||
|
|
|
@ -98,7 +98,7 @@ RSpec.describe DossierMailer, type: :mailer do
|
|||
|
||||
describe '.notify_automatic_deletion_to_user' do
|
||||
let(:dossier) { create(:dossier) }
|
||||
let(:deleted_dossier) { DeletedDossier.create_from_dossier(dossier) }
|
||||
let(:deleted_dossier) { DeletedDossier.create_from_dossier(dossier, :expired) }
|
||||
|
||||
before do
|
||||
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
|
||||
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) }
|
||||
|
||||
|
|
|
@ -53,6 +53,10 @@ class DossierMailerPreview < ActionMailer::Preview
|
|||
DossierMailer.notify_automatic_deletion_to_administration([deleted_dossier, deleted_dossier], administration_email)
|
||||
end
|
||||
|
||||
def notify_brouillon_not_submitted
|
||||
DossierMailer.notify_brouillon_not_submitted(draft)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def usager_email
|
||||
|
@ -76,7 +80,7 @@ class DossierMailerPreview < ActionMailer::Preview
|
|||
end
|
||||
|
||||
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
|
||||
|
||||
def service
|
||||
|
|
|
@ -46,7 +46,7 @@ describe Dossier do
|
|||
end
|
||||
|
||||
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!(: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) }
|
||||
|
@ -63,7 +63,7 @@ describe Dossier do
|
|||
end
|
||||
|
||||
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!(: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) }
|
||||
|
@ -77,10 +77,19 @@ describe Dossier do
|
|||
is_expected.to include(just_expired_dossier)
|
||||
is_expected.to include(long_expired_dossier)
|
||||
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
|
||||
|
||||
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!(: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) }
|
||||
|
@ -441,7 +450,7 @@ describe Dossier do
|
|||
end
|
||||
|
||||
describe "#unfollow_stale_instructeurs" do
|
||||
let(:procedure) { create(:procedure) }
|
||||
let(:procedure) { create(:procedure, :published) }
|
||||
let(:instructeur) { create(:instructeur) }
|
||||
let(:new_groupe_instructeur) { create(:groupe_instructeur, procedure: procedure) }
|
||||
let(:instructeur2) { create(:instructeur, groupe_instructeurs: [procedure.defaut_groupe_instructeur, new_groupe_instructeur]) }
|
||||
|
@ -661,64 +670,110 @@ describe Dossier do
|
|||
end
|
||||
end
|
||||
|
||||
describe "#delete_and_keep_track" do
|
||||
let(:dossier) { create(:dossier) }
|
||||
let(:deleted_dossier) { DeletedDossier.find_by!(dossier_id: dossier.id) }
|
||||
describe "#delete_and_keep_track!" do
|
||||
let(:dossier) { create(:dossier, :en_construction) }
|
||||
let(:deleted_dossier) { DeletedDossier.find_by(dossier_id: dossier.id) }
|
||||
let(:last_operation) { dossier.dossier_operation_logs.last }
|
||||
let(:reason) { :user_request }
|
||||
|
||||
before do
|
||||
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))
|
||||
end
|
||||
|
||||
subject! { dossier.delete_and_keep_track(dossier.user) }
|
||||
subject! { dossier.delete_and_keep_track!(dossier.user, reason) }
|
||||
|
||||
it 'hides the dossier' do
|
||||
expect(dossier.hidden_at).to be_present
|
||||
end
|
||||
|
||||
it 'creates a DeletedDossier record' do
|
||||
expect(deleted_dossier.dossier_id).to eq dossier.id
|
||||
expect(deleted_dossier.procedure).to eq dossier.procedure
|
||||
expect(deleted_dossier.state).to eq dossier.state
|
||||
expect(deleted_dossier.deleted_at).to be_present
|
||||
end
|
||||
|
||||
it 'notifies the user' do
|
||||
expect(DossierMailer).to have_received(:notify_deletion_to_user).with(deleted_dossier, dossier.user.email)
|
||||
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
|
||||
|
||||
context 'where instructeurs are following the dossier' do
|
||||
let(:dossier) { create(:dossier, :en_construction, :followed) }
|
||||
let!(:non_following_instructeur) do
|
||||
non_following_instructeur = create(:instructeur)
|
||||
non_following_instructeur.groupe_instructeurs << dossier.procedure.defaut_groupe_instructeur
|
||||
non_following_instructeur
|
||||
end
|
||||
|
||||
it 'notifies the following instructeurs' do
|
||||
expect(DossierMailer).to have_received(:notify_deletion_to_administration).once
|
||||
expect(DossierMailer).to have_received(:notify_deletion_to_administration).with(deleted_dossier, dossier.followers_instructeurs.first.email)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when there are no following instructeurs' do
|
||||
let(:dossier) { create(:dossier, :en_construction) }
|
||||
it 'notifies the procedure administrateur' do
|
||||
expect(DossierMailer).to have_received(:notify_deletion_to_administration).once
|
||||
expect(DossierMailer).to have_received(:notify_deletion_to_administration).with(deleted_dossier, dossier.procedure.administrateurs.first.email)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when dossier is brouillon' do
|
||||
context 'brouillon' do
|
||||
let(:dossier) { create(:dossier) }
|
||||
it 'do not notifies the procedure administrateur' do
|
||||
expect(DossierMailer).not_to have_received(:notify_deletion_to_administration)
|
||||
|
||||
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
|
||||
expect(dossier.hidden_at).to be_present
|
||||
end
|
||||
|
||||
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.procedure).to eq dossier.procedure
|
||||
expect(deleted_dossier.state).to eq dossier.state
|
||||
expect(deleted_dossier.deleted_at).to be_present
|
||||
end
|
||||
|
||||
it 'notifies the user' do
|
||||
expect(DossierMailer).to have_received(:notify_deletion_to_user).with(deleted_dossier, dossier.user.email)
|
||||
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
|
||||
|
||||
context 'where instructeurs are following the dossier' do
|
||||
let(:dossier) { create(:dossier, :en_construction, :followed) }
|
||||
let!(:non_following_instructeur) do
|
||||
non_following_instructeur = create(:instructeur)
|
||||
non_following_instructeur.groupe_instructeurs << dossier.procedure.defaut_groupe_instructeur
|
||||
non_following_instructeur
|
||||
end
|
||||
|
||||
it 'notifies the following instructeurs' do
|
||||
expect(DossierMailer).to have_received(:notify_deletion_to_administration).once
|
||||
expect(DossierMailer).to have_received(:notify_deletion_to_administration).with(deleted_dossier, dossier.followers_instructeurs.first.email)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when there are no following instructeurs' do
|
||||
let(:dossier) { create(:dossier, :en_construction) }
|
||||
it 'notifies the procedure administrateur' do
|
||||
expect(DossierMailer).to have_received(:notify_deletion_to_administration).once
|
||||
expect(DossierMailer).to have_received(:notify_deletion_to_administration).with(deleted_dossier, dossier.procedure.administrateurs.first.email)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when dossier is brouillon' do
|
||||
let(:dossier) { create(:dossier) }
|
||||
it 'do not notifies the procedure administrateur' do
|
||||
expect(DossierMailer).not_to have_received(:notify_deletion_to_administration)
|
||||
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
|
||||
|
@ -1097,6 +1152,33 @@ describe Dossier do
|
|||
it { expect(Dossier.for_procedure(procedure_2)).to contain_exactly(dossier_2_1) }
|
||||
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
|
||||
let(:lat) { "46.538192" }
|
||||
let(:lon) { "2.428462" }
|
||||
|
@ -1131,4 +1213,41 @@ describe Dossier do
|
|||
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
|
||||
|
|
|
@ -264,7 +264,7 @@ describe User, type: :model do
|
|||
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_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
|
||||
end
|
||||
end
|
||||
|
@ -278,16 +278,16 @@ describe User, type: :model do
|
|||
end
|
||||
|
||||
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)
|
||||
|
||||
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
|
||||
end
|
||||
|
||||
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)
|
||||
|
||||
expect(Dossier.find_by(id: dossier_from_another_user.id)).to be_present
|
||||
|
|
|
@ -4,7 +4,7 @@ describe ExpiredDossiersDeletionService do
|
|||
describe '#process_expired_dossiers_brouillon' do
|
||||
let(:draft_expiration) { 1.month + 5.days }
|
||||
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_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 }
|
||||
|
@ -34,7 +34,7 @@ describe ExpiredDossiersDeletionService 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).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)
|
||||
end
|
||||
end
|
||||
|
@ -192,7 +192,7 @@ describe ExpiredDossiersDeletionService do
|
|||
end
|
||||
|
||||
context 'when an instructeur is also administrateur' do
|
||||
let!(:procedure) { create(:procedure) }
|
||||
let!(:procedure) { create(:procedure, :published) }
|
||||
let!(:administrateur) { procedure.administrateurs.first }
|
||||
let!(:dossier) { create(:dossier, :en_construction, procedure: procedure, en_construction_at: (conservation_par_defaut - 1.month + 1.day).ago) }
|
||||
|
||||
|
|
Loading…
Reference in a new issue