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)
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)

View file

@ -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

View file

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

View file

@ -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;

View file

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

View file

@ -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;
}
}

View file

@ -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;

View file

@ -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;
}

View file

@ -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]

View file

@ -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é."

View file

@ -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

View file

@ -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,

View file

@ -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?

View file

@ -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();

View file

@ -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

View file

@ -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';

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)
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

View file

@ -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)) }

View file

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

View file

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

View file

@ -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

View file

@ -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)
}
}

View file

@ -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)
}
}

View file

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

View file

@ -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

View file

@ -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

View file

@ -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)

View file

@ -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!

View file

@ -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 }

View file

@ -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}") %>

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.
- 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

View file

@ -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é

View file

@ -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'

View file

@ -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

View file

@ -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

View file

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

View file

@ -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

View file

@ -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

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 @@
= 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)

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
= form.radio_button :value, Individual::GENDER_MALE
Monsieur

View file

@ -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? },

View file

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

View file

@ -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.

View file

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

View file

@ -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

View file

@ -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)

View file

@ -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 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
= dynamic_tab_item('Résumé', dossier_path(dossier))
= dynamic_tab_item('Demande', [demande_dossier_path(dossier), modifier_dossier_path(dossier)])

View file

@ -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"

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"
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"

View file

@ -1,5 +1,5 @@
fr:
activerecord:
attributes:
champs:
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'
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

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.
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"

View file

@ -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]) }

View file

@ -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')

View file

@ -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'

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_procedure_description(procedure)
choose 'M.'
fill_in 'individual_nom', with: 'Nom'
fill_in 'individual_prenom', with: 'Prenom'
end

View file

@ -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'

View file

@ -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) }

View file

@ -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) }

View file

@ -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) }

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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) }