diff --git a/Gemfile.lock b/Gemfile.lock index 425c59161..d28ce5d8a 100644 --- a/Gemfile.lock +++ b/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) diff --git a/README.md b/README.md index abd5ae103..e60b47848 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/app/assets/stylesheets/new_design/dossiers_table.scss b/app/assets/stylesheets/new_design/dossiers_table.scss index 5dab7fa66..1af317cdb 100644 --- a/app/assets/stylesheets/new_design/dossiers_table.scss +++ b/app/assets/stylesheets/new_design/dossiers_table.scss @@ -32,6 +32,10 @@ display: block; } + .deleted-cell { + padding: (2 * $default-spacer) $default-spacer; + } + .icon.folder { position: relative; diff --git a/app/assets/stylesheets/new_design/forms.scss b/app/assets/stylesheets/new_design/forms.scss index aa201b22d..031c11f83 100644 --- a/app/assets/stylesheets/new_design/forms.scss +++ b/app/assets/stylesheets/new_design/forms.scss @@ -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; diff --git a/app/assets/stylesheets/new_design/invites_form.scss b/app/assets/stylesheets/new_design/invites_form.scss index 81580da30..2cd6e65e3 100644 --- a/app/assets/stylesheets/new_design/invites_form.scss +++ b/app/assets/stylesheets/new_design/invites_form.scss @@ -26,7 +26,7 @@ input[type=email] { width: auto; - margin-bottom: 0; + margin-bottom: $default-spacer; } .button { diff --git a/app/assets/stylesheets/new_design/labels.scss b/app/assets/stylesheets/new_design/labels.scss index 4a4cedf5f..9b0e797b3 100644 --- a/app/assets/stylesheets/new_design/labels.scss +++ b/app/assets/stylesheets/new_design/labels.scss @@ -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; + } } diff --git a/app/assets/stylesheets/new_design/new_header.scss b/app/assets/stylesheets/new_design/new_header.scss index 5dd761b73..27cac962d 100644 --- a/app/assets/stylesheets/new_design/new_header.scss +++ b/app/assets/stylesheets/new_design/new_header.scss @@ -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; diff --git a/app/assets/stylesheets/new_design/procedure_show.scss b/app/assets/stylesheets/new_design/procedure_show.scss index 5f9c31bb9..9303ad8ca 100644 --- a/app/assets/stylesheets/new_design/procedure_show.scss +++ b/app/assets/stylesheets/new_design/procedure_show.scss @@ -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; } diff --git a/app/controllers/instructeurs/procedures_controller.rb b/app/controllers/instructeurs/procedures_controller.rb index 4726bd12b..4fce78b3f 100644 --- a/app/controllers/instructeurs/procedures_controller.rb +++ b/app/controllers/instructeurs/procedures_controller.rb @@ -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] diff --git a/app/controllers/manager/dossiers_controller.rb b/app/controllers/manager/dossiers_controller.rb index 8fd264a62..c4c076cd8 100644 --- a/app/controllers/manager/dossiers_controller.rb +++ b/app/controllers/manager/dossiers_controller.rb @@ -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é." diff --git a/app/controllers/users/dossiers_controller.rb b/app/controllers/users/dossiers_controller.rb index 8d4ffd26a..f90716df6 100644 --- a/app/controllers/users/dossiers_controller.rb +++ b/app/controllers/users/dossiers_controller.rb @@ -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 diff --git a/app/dashboards/procedure_dashboard.rb b/app/dashboards/procedure_dashboard.rb index fa9175e32..076df9cb3 100644 --- a/app/dashboards/procedure_dashboard.rb +++ b/app/dashboards/procedure_dashboard.rb @@ -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, diff --git a/app/helpers/dossier_helper.rb b/app/helpers/dossier_helper.rb index 50a24a464..1341eeaaa 100644 --- a/app/helpers/dossier_helper.rb +++ b/app/helpers/dossier_helper.rb @@ -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? diff --git a/app/javascript/new_design/autosave-controller.js b/app/javascript/new_design/dossiers/auto-save-controller.js similarity index 98% rename from app/javascript/new_design/autosave-controller.js rename to app/javascript/new_design/dossiers/auto-save-controller.js index 3d15407a4..83bc1a2e1 100644 --- a/app/javascript/new_design/autosave-controller.js +++ b/app/javascript/new_design/dossiers/auto-save-controller.js @@ -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(); diff --git a/app/javascript/new_design/autosave.js b/app/javascript/new_design/dossiers/auto-save.js similarity index 91% rename from app/javascript/new_design/autosave.js rename to app/javascript/new_design/dossiers/auto-save.js index 9e03fa632..0bdfe9870 100644 --- a/app/javascript/new_design/autosave.js +++ b/app/javascript/new_design/dossiers/auto-save.js @@ -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 diff --git a/app/javascript/packs/application.js b/app/javascript/packs/application.js index 7689c88d6..5d33fb7ae 100644 --- a/app/javascript/packs/application.js +++ b/app/javascript/packs/application.js @@ -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'; diff --git a/app/jobs/discarded_dossiers_deletion_job.rb b/app/jobs/discarded_dossiers_deletion_job.rb new file mode 100644 index 000000000..4fc061ad5 --- /dev/null +++ b/app/jobs/discarded_dossiers_deletion_job.rb @@ -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 diff --git a/app/jobs/notify_draft_not_submitted_job.rb b/app/jobs/notify_draft_not_submitted_job.rb new file mode 100644 index 000000000..82958c538 --- /dev/null +++ b/app/jobs/notify_draft_not_submitted_job.rb @@ -0,0 +1,7 @@ +class NotifyDraftNotSubmittedJob < ApplicationJob + queue_as :cron + + def perform(*args) + Dossier.notify_draft_not_submitted + end +end diff --git a/app/mailers/dossier_mailer.rb b/app/mailers/dossier_mailer.rb index b3fcd9a94..6bc61df26 100644 --- a/app/mailers/dossier_mailer.rb +++ b/app/mailers/dossier_mailer.rb @@ -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 diff --git a/app/models/administrateur.rb b/app/models/administrateur.rb index 2f57bf5bc..8082325d3 100644 --- a/app/models/administrateur.rb +++ b/app/models/administrateur.rb @@ -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)) } diff --git a/app/models/champ.rb b/app/models/champ.rb index 69c6303e3..35936912b 100644 --- a/app/models/champ.rb +++ b/app/models/champ.rb @@ -80,6 +80,10 @@ class Champ < ApplicationRecord type_de_champ.to_typed_id end + def html_label? + true + end + private def needs_dossier_id? diff --git a/app/models/champs/civilite_champ.rb b/app/models/champs/civilite_champ.rb index 9404e46a1..600a2c032 100644 --- a/app/models/champs/civilite_champ.rb +++ b/app/models/champs/civilite_champ.rb @@ -1,2 +1,5 @@ class Champs::CiviliteChamp < Champ + def html_label? + false + end end diff --git a/app/models/champs/datetime_champ.rb b/app/models/champs/datetime_champ.rb index 84008894b..7f09acee1 100644 --- a/app/models/champs/datetime_champ.rb +++ b/app/models/champs/datetime_champ.rb @@ -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 diff --git a/app/models/champs/decimal_number_champ.rb b/app/models/champs/decimal_number_champ.rb index d7063cf8e..31278e52a 100644 --- a/app/models/champs/decimal_number_champ.rb +++ b/app/models/champs/decimal_number_champ.rb @@ -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) } } diff --git a/app/models/champs/integer_number_champ.rb b/app/models/champs/integer_number_champ.rb index c2c0ea254..082c73593 100644 --- a/app/models/champs/integer_number_champ.rb +++ b/app/models/champs/integer_number_champ.rb @@ -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) } } diff --git a/app/models/champs/piece_justificative_champ.rb b/app/models/champs/piece_justificative_champ.rb index 35403deb6..5677ae7d5 100644 --- a/app/models/champs/piece_justificative_champ.rb +++ b/app/models/champs/piece_justificative_champ.rb @@ -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 diff --git a/app/models/deleted_dossier.rb b/app/models/deleted_dossier.rb index 25338f6b4..bce5518ee 100644 --- a/app/models/deleted_dossier.rb +++ b/app/models/deleted_dossier.rb @@ -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 diff --git a/app/models/dossier.rb b/app/models/dossier.rb index f0eef0d1d..56169df18 100644 --- a/app/models/dossier.rb +++ b/app/models/dossier.rb @@ -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 diff --git a/app/models/dossier_operation_log.rb b/app/models/dossier_operation_log.rb index bfad08b97..f1cd8ce1e 100644 --- a/app/models/dossier_operation_log.rb +++ b/app/models/dossier_operation_log.rb @@ -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) diff --git a/app/models/user.rb b/app/models/user.rb index 72dc84cc4..0657a4b17 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -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! diff --git a/app/services/expired_dossiers_deletion_service.rb b/app/services/expired_dossiers_deletion_service.rb index 6129b5331..e47fff4e2 100644 --- a/app/services/expired_dossiers_deletion_service.rb +++ b/app/services/expired_dossiers_deletion_service.rb @@ -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 } diff --git a/app/views/attachments/destroy.js.erb b/app/views/attachments/destroy.js.erb index 4d5054b7e..0d4b5e5ff 100644 --- a/app/views/attachments/destroy.js.erb +++ b/app/views/attachments/destroy.js.erb @@ -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}") %> diff --git a/app/views/dossier_mailer/notify_brouillon_not_submitted.html.haml b/app/views/dossier_mailer/notify_brouillon_not_submitted.html.haml new file mode 100644 index 000000000..fe4e33910 --- /dev/null +++ b/app/views/dossier_mailer/notify_brouillon_not_submitted.html.haml @@ -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" diff --git a/app/views/instructeurs/procedures/deleted_dossiers.html.haml b/app/views/instructeurs/procedures/deleted_dossiers.html.haml new file mode 100644 index 000000000..bca8319cc --- /dev/null +++ b/app/views/instructeurs/procedures/deleted_dossiers.html.haml @@ -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é + diff --git a/app/views/instructeurs/procedures/show.html.haml b/app/views/instructeurs/procedures/show.html.haml index a93ef3493..ba85ed00e 100644 --- a/app/views/instructeurs/procedures/show.html.haml +++ b/app/views/instructeurs/procedures/show.html.haml @@ -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 diff --git a/app/views/instructeurs/recherche/index.html.haml b/app/views/instructeurs/recherche/index.html.haml index 635895a60..612e1a616 100644 --- a/app/views/instructeurs/recherche/index.html.haml +++ b/app/views/instructeurs/recherche/index.html.haml @@ -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é diff --git a/app/views/invites/_form.html.haml b/app/views/invites/_form.html.haml index 7936bc7ce..5ebaeca70 100644 --- a/app/views/invites/_form.html.haml +++ b/app/views/invites/_form.html.haml @@ -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' diff --git a/app/views/layouts/_account_dropdown.haml b/app/views/layouts/_account_dropdown.haml index a897363a7..e826f372c 100644 --- a/app/views/layouts/_account_dropdown.haml +++ b/app/views/layouts/_account_dropdown.haml @@ -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 diff --git a/app/views/layouts/_new_header.haml b/app/views/layouts/_new_header.haml index fae04f47a..e172edf61 100644 --- a/app/views/layouts/_new_header.haml +++ b/app/views/layouts/_new_header.haml @@ -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 diff --git a/app/views/root/accessibilite.html.haml b/app/views/root/accessibilite.html.haml index 5ed1165dd..921bab63a 100644 --- a/app/views/root/accessibilite.html.haml +++ b/app/views/root/accessibilite.html.haml @@ -1,4 +1,6 @@ - content_for(:title, 'Accessibilité') +- content_for :footer do + = render partial: "root/footer" .accessibilite diff --git a/app/views/root/suivi.html.haml b/app/views/root/suivi.html.haml index 12f4a963b..4cd6e9dde 100644 --- a/app/views/root/suivi.html.haml +++ b/app/views/root/suivi.html.haml @@ -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 diff --git a/app/views/shared/attachment/_edit.html.haml b/app/views/shared/attachment/_edit.html.haml index 2a12e2f74..e250fd6e8 100644 --- a/app/views/shared/attachment/_edit.html.haml +++ b/app/views/shared/attachment/_edit.html.haml @@ -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 diff --git a/app/views/shared/dossiers/_status_badge.html.haml b/app/views/shared/dossiers/_status_badge.html.haml deleted file mode 100644 index 775a93b0f..000000000 --- a/app/views/shared/dossiers/_status_badge.html.haml +++ /dev/null @@ -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 diff --git a/app/views/shared/dossiers/editable_champs/_champ_label.html.haml b/app/views/shared/dossiers/editable_champs/_champ_label.html.haml index 4ca8243dc..737846f64 100644 --- a/app/views/shared/dossiers/editable_champs/_champ_label.html.haml +++ b/app/views/shared/dossiers/editable_champs/_champ_label.html.haml @@ -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) diff --git a/app/views/shared/dossiers/editable_champs/_champ_label_content.html.haml b/app/views/shared/dossiers/editable_champs/_champ_label_content.html.haml new file mode 100644 index 000000000..7174f7ee5 --- /dev/null +++ b/app/views/shared/dossiers/editable_champs/_champ_label_content.html.haml @@ -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)}" diff --git a/app/views/shared/dossiers/editable_champs/_civilite.html.haml b/app/views/shared/dossiers/editable_champs/_civilite.html.haml index b20e50673..9c4d3bfac 100644 --- a/app/views/shared/dossiers/editable_champs/_civilite.html.haml +++ b/app/views/shared/dossiers/editable_champs/_civilite.html.haml @@ -1,4 +1,6 @@ -.radios +%fieldset.radios + %legend.mandatory-explanation + Sélectionnez une des valeurs %label = form.radio_button :value, Individual::GENDER_MALE Monsieur diff --git a/app/views/shared/dossiers/editable_champs/_linked_drop_down_list.html.haml b/app/views/shared/dossiers/editable_champs/_linked_drop_down_list.html.haml index fcabeefa9..259aa3bc1 100644 --- a/app/views/shared/dossiers/editable_champs/_linked_drop_down_list.html.haml +++ b/app/views/shared/dossiers/editable_champs/_linked_drop_down_list.html.haml @@ -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? }, diff --git a/app/views/shared/dossiers/editable_champs/_yes_no.html.haml b/app/views/shared/dossiers/editable_champs/_yes_no.html.haml index 0d8b3bcb0..11cd71ad0 100644 --- a/app/views/shared/dossiers/editable_champs/_yes_no.html.haml +++ b/app/views/shared/dossiers/editable_champs/_yes_no.html.haml @@ -1,4 +1,6 @@ -.radios +%fieldset.radios + %legend.mandatory-explanation + Sélectionnez une des deux valeurs %label = form.radio_button :value, true Oui diff --git a/app/views/stats/index.html.haml b/app/views/stats/index.html.haml index 8e0167a84..6f60f2628 100644 --- a/app/views/stats/index.html.haml +++ b/app/views/stats/index.html.haml @@ -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. diff --git a/app/views/support/index.html.haml b/app/views/support/index.html.haml index 02dfa31c9..61d6302d8 100644 --- a/app/views/support/index.html.haml +++ b/app/views/support/index.html.haml @@ -1,4 +1,6 @@ - content_for(:title, 'Contact') +- content_for :footer do + = render partial: "root/footer" #contact-form .container diff --git a/app/views/users/dossiers/identite.html.haml b/app/views/users/dossiers/identite.html.haml index bf151d17f..1e65a5123 100644 --- a/app/views/users/dossiers/identite.html.haml +++ b/app/views/users/dossiers/identite.html.haml @@ -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 diff --git a/app/views/users/dossiers/index.html.haml b/app/views/users/dossiers/index.html.haml index fc4f1d4ab..b6e169488 100644 --- a/app/views/users/dossiers/index.html.haml +++ b/app/views/users/dossiers/index.html.haml @@ -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) diff --git a/app/views/users/dossiers/show/_header.html.haml b/app/views/users/dossiers/show/_header.html.haml index e148b90d7..8948086d2 100644 --- a/app/views/users/dossiers/show/_header.html.haml +++ b/app/views/users/dossiers/show/_header.html.haml @@ -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)]) diff --git a/app/views/users/registrations/new.html.haml b/app/views/users/registrations/new.html.haml index 7a83cc034..e6602c9b5 100644 --- a/app/views/users/registrations/new.html.haml +++ b/app/views/users/registrations/new.html.haml @@ -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" diff --git a/config/initializers/date_select.rb b/config/initializers/date_select.rb new file mode 100644 index 000000000..8418f043a --- /dev/null +++ b/config/initializers/date_select.rb @@ -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]}" + + "" + 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 diff --git a/config/initializers/pg_interval_5_2.rb b/config/initializers/pg_interval_5_2.rb new file mode 100644 index 000000000..fbd6049b0 --- /dev/null +++ b/config/initializers/pg_interval_5_2.rb @@ -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) diff --git a/config/initializers/urls.rb b/config/initializers/urls.rb index 00b747247..6207c799a 100644 --- a/config/initializers/urls.rb +++ b/config/initializers/urls.rb @@ -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" diff --git a/config/locales/models/champs/fr.yml b/config/locales/models/champs/fr.yml index d1ab85d27..9d9e6b2f0 100644 --- a/config/locales/models/champs/fr.yml +++ b/config/locales/models/champs/fr.yml @@ -1,5 +1,5 @@ fr: activerecord: attributes: - champs: + champ: value: La valeur du champ diff --git a/config/locales/models/deleted_dossier/fr.yml b/config/locales/models/deleted_dossier/fr.yml new file mode 100644 index 000000000..5b4dd95e1 --- /dev/null +++ b/config/locales/models/deleted_dossier/fr.yml @@ -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 + diff --git a/config/routes.rb b/config/routes.rb index 32bc4a620..e3c7f3552 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -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 diff --git a/db/migrate/20200319101825_add_en_construction_conservation_extension_to_dossiers.rb b/db/migrate/20200319101825_add_en_construction_conservation_extension_to_dossiers.rb new file mode 100644 index 000000000..a0b9a241e --- /dev/null +++ b/db/migrate/20200319101825_add_en_construction_conservation_extension_to_dossiers.rb @@ -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 diff --git a/db/migrate/20200319103836_add_reason_to_deleted_dossiers.rb b/db/migrate/20200319103836_add_reason_to_deleted_dossiers.rb new file mode 100644 index 000000000..4b882b537 --- /dev/null +++ b/db/migrate/20200319103836_add_reason_to_deleted_dossiers.rb @@ -0,0 +1,5 @@ +class AddReasonToDeletedDossiers < ActiveRecord::Migration[5.2] + def change + add_column :deleted_dossiers, :reason, :string + end +end diff --git a/db/schema.rb b/db/schema.rb index a7a596376..02f9e1b83 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -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" diff --git a/spec/controllers/instructeurs/procedures_controller_spec.rb b/spec/controllers/instructeurs/procedures_controller_spec.rb index 4fde099fa..638d4313b 100644 --- a/spec/controllers/instructeurs/procedures_controller_spec.rb +++ b/spec/controllers/instructeurs/procedures_controller_spec.rb @@ -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]) } diff --git a/spec/features/routing/full_scenario_spec.rb b/spec/features/routing/full_scenario_spec.rb index b1bdb5c70..0401a8ade 100644 --- a/spec/features/routing/full_scenario_spec.rb +++ b/spec/features/routing/full_scenario_spec.rb @@ -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') diff --git a/spec/features/users/brouillon_spec.rb b/spec/features/users/brouillon_spec.rb index 3acd1adc2..9de07012e 100644 --- a/spec/features/users/brouillon_spec.rb +++ b/spec/features/users/brouillon_spec.rb @@ -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 + # + #