commit
10141e9176
42 changed files with 803 additions and 115 deletions
|
@ -176,7 +176,7 @@
|
|||
margin-bottom: $default-fields-spacer;
|
||||
|
||||
&.small-margin {
|
||||
margin-bottom: $default-padding / 2;
|
||||
margin-bottom: $default-spacer;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -186,7 +186,7 @@
|
|||
|
||||
input[type=checkbox] {
|
||||
&.small-margin {
|
||||
margin-bottom: $default-padding / 2;
|
||||
margin-bottom: $default-spacer;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -203,7 +203,7 @@
|
|||
padding: $default-padding;
|
||||
|
||||
&.small {
|
||||
padding: $default-padding / 2;
|
||||
padding: $default-spacer;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
|
@ -292,12 +292,13 @@
|
|||
|
||||
|
||||
[data-reach-combobox-input] {
|
||||
padding: 16px;
|
||||
width: 100%;
|
||||
min-width: 50%;
|
||||
max-width: 100%;
|
||||
min-height: 62px;
|
||||
margin-bottom: 40px;
|
||||
border-radius: 4px;
|
||||
border: solid 1px $border-grey;
|
||||
padding: $default-padding;
|
||||
}
|
||||
|
||||
[data-reach-combobox-input]:focus {
|
||||
|
@ -305,25 +306,31 @@
|
|||
}
|
||||
|
||||
[data-reach-combobox-token-list] {
|
||||
padding: $default-padding;
|
||||
padding: $default-spacer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
[data-reach-combobox-token] {
|
||||
border: solid 1px $border-grey;
|
||||
color: $black;
|
||||
margin-top: $default-padding;
|
||||
margin-bottom: $default-padding;
|
||||
margin-right: 0.5 * $default-padding;
|
||||
border-radius: 4px;
|
||||
padding: $default-padding;
|
||||
padding: $default-spacer;
|
||||
margin-right: $default-spacer;
|
||||
cursor: pointer;
|
||||
list-style: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
[data-reach-combobox-token]:focus {
|
||||
background-color: $black;
|
||||
color: $white;
|
||||
|
||||
[data-combobox-remove-token] {
|
||||
background-color: $black;
|
||||
color: $white;
|
||||
}
|
||||
}
|
||||
|
||||
.editable-champ {
|
||||
|
@ -388,7 +395,7 @@
|
|||
|
||||
.explication {
|
||||
margin-bottom: $default-fields-spacer;
|
||||
padding: $default-padding / 2;
|
||||
padding: $default-spacer;
|
||||
background-color: $light-grey;
|
||||
|
||||
p:not(:last-child) {
|
||||
|
@ -467,23 +474,26 @@
|
|||
}
|
||||
}
|
||||
|
||||
[data-react-class]:not([data-react-class="ComboMultipleDropdownList"]) {
|
||||
[data-reach-combobox-input] {
|
||||
margin-bottom: $default-fields-spacer;
|
||||
}
|
||||
}
|
||||
|
||||
[data-react-class="ComboMultipleDropdownList"] {
|
||||
margin-bottom: $default-fields-spacer;
|
||||
|
||||
[data-reach-combobox-input] {
|
||||
outline: none;
|
||||
border: none;
|
||||
flex-grow: 1;
|
||||
margin: 0.25rem;
|
||||
background-image: image-url("icons/chevron-down");
|
||||
background-size: 14px;
|
||||
background-repeat: no-repeat;
|
||||
background-position: right 10px center;
|
||||
}
|
||||
|
||||
[data-reach-combobox-input]:focus {
|
||||
outline: solid;
|
||||
outline-color: $light-blue;
|
||||
border-radius: 4px;
|
||||
border: solid 1px $border-grey;
|
||||
padding: $default-padding;
|
||||
margin: $default-spacer;
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -507,15 +517,26 @@
|
|||
[data-combobox-separator] {
|
||||
font-size: 16px;
|
||||
color: $dark-grey;
|
||||
margin-top: 6px;
|
||||
background: $light-grey;
|
||||
padding: $default-spacer;
|
||||
}
|
||||
|
||||
[data-combobox-remove-token] {
|
||||
color: $dark-grey;
|
||||
padding-right: 4px;
|
||||
cursor: pointer;
|
||||
background-color: transparent;
|
||||
background-image: none;
|
||||
border: none;
|
||||
line-height: 1;
|
||||
padding: 0;
|
||||
margin-right: 4px;
|
||||
display: flex;
|
||||
align-items: center !important;
|
||||
}
|
||||
|
||||
|
||||
[data-reach-combobox-input]:focus {
|
||||
outline-color: $light-blue;
|
||||
}
|
||||
|
||||
[data-reach-combobox-popover] {
|
||||
z-index: 20;
|
||||
}
|
||||
|
|
|
@ -79,24 +79,33 @@
|
|||
padding: 0.25 * $default-padding;
|
||||
cursor: pointer;
|
||||
list-style: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
[data-reach-combobox-token]:focus {
|
||||
background-color: $black;
|
||||
color: $white;
|
||||
|
||||
[data-combobox-remove-token] {
|
||||
background-color: $black;
|
||||
color: $white;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
[data-reach-combobox-input] {
|
||||
outline: none;
|
||||
border: none;
|
||||
flex-grow: 1;
|
||||
margin: 0.25rem;
|
||||
margin: $default-spacer;
|
||||
padding: $default-spacer;
|
||||
border-radius: 4px;
|
||||
border: solid 1px $border-grey;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
[data-reach-combobox-input]:focus {
|
||||
outline: solid;
|
||||
outline-color: $light-blue;
|
||||
border-color: $blue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
55
app/controllers/instructeurs/archives_controller.rb
Normal file
55
app/controllers/instructeurs/archives_controller.rb
Normal file
|
@ -0,0 +1,55 @@
|
|||
module Instructeurs
|
||||
class ArchivesController < InstructeurController
|
||||
before_action :ensure_procedure_enabled
|
||||
|
||||
def index
|
||||
@procedure = procedure
|
||||
|
||||
@archivable_months = archivable_months
|
||||
@dossiers_termines = @procedure.dossiers.state_termine
|
||||
@poids_total = ProcedureArchiveService.procedure_files_size(@procedure)
|
||||
groupe_instructeur = current_instructeur.groupe_instructeurs.where(procedure: @procedure.id).first
|
||||
@archives = Archive.for_groupe_instructeur(groupe_instructeur)
|
||||
end
|
||||
|
||||
def create
|
||||
type = params[:type]
|
||||
month = Date.strptime(params[:month], '%Y-%m') if params[:month].present?
|
||||
|
||||
archive = ProcedureArchiveService.new(procedure).create_pending_archive(current_instructeur, type, month)
|
||||
if archive.pending?
|
||||
ArchiveCreationJob.perform_later(procedure, archive, current_instructeur)
|
||||
flash[:notice] = "Votre demande a été prise en compte. Selon le nombre de dossiers, cela peut prendre quelques minutes. Vous recevrez un courriel lorsque le fichier sera disponible."
|
||||
else
|
||||
flash[:notice] = "Cette archive a déjà été générée."
|
||||
end
|
||||
redirect_to instructeur_archives_path(procedure)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def ensure_procedure_enabled
|
||||
if !procedure.feature_enabled?(:archive_zip_globale) || procedure.brouillon?
|
||||
flash[:alert] = "L'accès aux archives n'est pas disponible pour cette démarche, merci d'en faire la demande à l'équipe de démarches simplifiees"
|
||||
return redirect_to instructeur_procedure_path(procedure)
|
||||
end
|
||||
end
|
||||
|
||||
def archivable_months
|
||||
start_date = procedure.published_at.to_date
|
||||
end_date = Time.zone.now.to_date
|
||||
|
||||
(start_date...end_date)
|
||||
.map(&:beginning_of_month)
|
||||
.uniq
|
||||
.reverse
|
||||
end
|
||||
|
||||
def procedure
|
||||
current_instructeur
|
||||
.procedures
|
||||
.for_download
|
||||
.find(params[:procedure_id])
|
||||
end
|
||||
end
|
||||
end
|
|
@ -213,7 +213,6 @@ module Instructeurs
|
|||
def telecharger_pjs
|
||||
return head(:forbidden) if !dossier.attachments_downloadable?
|
||||
|
||||
generate_pdf_for_instructeur_export
|
||||
files = ActiveStorage::DownloadableFile.create_list_from_dossier(dossier)
|
||||
|
||||
zipline(files, "dossier-#{dossier.id}.zip")
|
||||
|
@ -239,12 +238,6 @@ module Instructeurs
|
|||
.find(params[:dossier_id])
|
||||
end
|
||||
|
||||
def generate_pdf_for_instructeur_export
|
||||
@include_infos_administration = true
|
||||
pdf = render_to_string(template: 'dossiers/show', formats: [:pdf])
|
||||
dossier.pdf_export_for_instructeur.attach(io: StringIO.open(pdf), filename: "export-#{dossier.id}.pdf", content_type: 'application/pdf')
|
||||
end
|
||||
|
||||
def commentaire_params
|
||||
params.require(:commentaire).permit(:body, :piece_jointe)
|
||||
end
|
||||
|
|
5
app/helpers/archive_helper.rb
Normal file
5
app/helpers/archive_helper.rb
Normal file
|
@ -0,0 +1,5 @@
|
|||
module ArchiveHelper
|
||||
def can_generate_archive?(dossiers_termines, poids_total)
|
||||
dossiers_termines.count < 100 && poids_total < 1.gigabyte
|
||||
end
|
||||
end
|
|
@ -18,6 +18,7 @@ import {
|
|||
import '@reach/combobox/styles.css';
|
||||
import { matchSorter } from 'match-sorter';
|
||||
import { fire } from '@utils';
|
||||
import { XIcon } from '@heroicons/react/outline';
|
||||
|
||||
const Context = createContext();
|
||||
|
||||
|
@ -152,7 +153,7 @@ function ComboMultipleDropdownList({
|
|||
/>
|
||||
</ComboboxTokenLabel>
|
||||
{results && (
|
||||
<ComboboxPopover portal={false}>
|
||||
<ComboboxPopover className="shadow-popup">
|
||||
{results.length === 0 && (
|
||||
<p>
|
||||
Aucun résultat{' '}
|
||||
|
@ -160,11 +161,17 @@ function ComboMultipleDropdownList({
|
|||
</p>
|
||||
)}
|
||||
<ComboboxList>
|
||||
{results.map(([label], index) => {
|
||||
{results.map(([label, value], index) => {
|
||||
if (label.startsWith('--')) {
|
||||
return <ComboboxSeparator key={index} value={label} />;
|
||||
}
|
||||
return <ComboboxOption key={index} value={label} />;
|
||||
return (
|
||||
<ComboboxOption
|
||||
key={index}
|
||||
value={label}
|
||||
data-option-value={value}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</ComboboxList>
|
||||
</ComboboxPopover>
|
||||
|
@ -226,15 +233,17 @@ function ComboboxToken({ value, ...props }) {
|
|||
}}
|
||||
{...props}
|
||||
>
|
||||
<span
|
||||
role="presentation"
|
||||
<button
|
||||
type="button"
|
||||
tabIndex={-1}
|
||||
data-combobox-remove-token
|
||||
onClick={() => {
|
||||
onRemove(value);
|
||||
}}
|
||||
>
|
||||
x
|
||||
</span>
|
||||
<XIcon style={{ width: '15px', height: '15px' }} />
|
||||
<span className="screen-reader-text">Désélectionner</span>
|
||||
</button>
|
||||
{value}
|
||||
</li>
|
||||
);
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { delegate } from '@utils';
|
||||
|
||||
delegate('click', 'body', (event) => {
|
||||
if (!event.target.closest('.dropdown')) {
|
||||
if (!event.target.closest('.dropdown, [data-reach-combobox-popover]')) {
|
||||
[...document.querySelectorAll('.dropdown')].forEach((element) => {
|
||||
const button = element.querySelector('.dropdown-button');
|
||||
button.setAttribute('aria-expanded', false);
|
||||
|
|
7
app/jobs/archive_creation_job.rb
Normal file
7
app/jobs/archive_creation_job.rb
Normal file
|
@ -0,0 +1,7 @@
|
|||
class ArchiveCreationJob < ApplicationJob
|
||||
def perform(procedure, archive, instructeur)
|
||||
ProcedureArchiveService
|
||||
.new(procedure)
|
||||
.collect_files_archive(archive, instructeur)
|
||||
end
|
||||
end
|
7
app/jobs/cron/purge_stale_archives_job.rb
Normal file
7
app/jobs/cron/purge_stale_archives_job.rb
Normal file
|
@ -0,0 +1,7 @@
|
|||
class Cron::PurgeStaleArchivesJob < Cron::CronJob
|
||||
self.schedule_expression = "every 5 minutes"
|
||||
|
||||
def perform
|
||||
Archive.stale.destroy_all
|
||||
end
|
||||
end
|
|
@ -1,14 +1,12 @@
|
|||
class ActiveStorage::DownloadableFile
|
||||
def self.create_list_from_dossier(dossier)
|
||||
pjs = PiecesJustificativesService.liste_pieces_justificatives(dossier)
|
||||
files = pjs.map do |piece_justificative|
|
||||
pjs.map do |piece_justificative|
|
||||
[
|
||||
piece_justificative,
|
||||
self.timestamped_filename(piece_justificative)
|
||||
"dossier-#{dossier.id}/#{self.timestamped_filename(piece_justificative)}"
|
||||
]
|
||||
end
|
||||
files << [dossier.pdf_export_for_instructeur, self.timestamped_filename(dossier.pdf_export_for_instructeur)]
|
||||
files
|
||||
end
|
||||
|
||||
private
|
||||
|
@ -22,19 +20,23 @@ class ActiveStorage::DownloadableFile
|
|||
timestamp = attachment.created_at.strftime("%d-%m-%Y-%H-%M")
|
||||
id = attachment.id % 10000
|
||||
|
||||
"#{folder}/#{basename}-#{timestamp}-#{id}#{extension}"
|
||||
[folder, "#{basename}-#{timestamp}-#{id}#{extension}"].join
|
||||
end
|
||||
|
||||
def self.folder(attachment)
|
||||
if attachment.name == 'pdf_export_for_instructeur'
|
||||
return ''
|
||||
end
|
||||
|
||||
case attachment.record_type
|
||||
when 'Dossier'
|
||||
'dossier'
|
||||
'dossier/'
|
||||
when 'DossierOperationLog', 'BillSignature'
|
||||
'horodatage'
|
||||
'horodatage/'
|
||||
when 'Commentaire'
|
||||
'messagerie'
|
||||
'messagerie/'
|
||||
else
|
||||
'pieces_justificatives'
|
||||
'pieces_justificatives/'
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
# Preview all emails at http://localhost:3000/rails/mailers/instructeur_mailer
|
||||
class InstructeurMailer < ApplicationMailer
|
||||
helper MailerHelper
|
||||
|
||||
layout 'mailers/layout'
|
||||
|
||||
def user_to_instructeur(email)
|
||||
|
@ -42,4 +44,12 @@ class InstructeurMailer < ApplicationMailer
|
|||
|
||||
mail(to: instructeur.email, subject: subject)
|
||||
end
|
||||
|
||||
def send_archive(instructeur, procedure, archive)
|
||||
@archive = archive
|
||||
@procedure = procedure
|
||||
subject = "Votre archive est disponible"
|
||||
|
||||
mail(to: instructeur.email, subject: subject)
|
||||
end
|
||||
end
|
||||
|
|
71
app/models/archive.rb
Normal file
71
app/models/archive.rb
Normal file
|
@ -0,0 +1,71 @@
|
|||
# == Schema Information
|
||||
#
|
||||
# Table name: archives
|
||||
#
|
||||
# id :bigint not null, primary key
|
||||
# key :text not null
|
||||
# month :date
|
||||
# status :string not null
|
||||
# time_span_type :string not null
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
#
|
||||
class Archive < ApplicationRecord
|
||||
include AASM
|
||||
|
||||
RETENTION_DURATION = 1.week
|
||||
|
||||
has_and_belongs_to_many :groupe_instructeurs
|
||||
|
||||
has_one_attached :file
|
||||
|
||||
scope :stale, -> { where('updated_at < ?', (Time.zone.now - RETENTION_DURATION)) }
|
||||
scope :for_groupe_instructeur, -> (groupe_instructeur) {
|
||||
joins(:archives_groupe_instructeurs)
|
||||
.where(
|
||||
archives_groupe_instructeurs: { groupe_instructeur: groupe_instructeur }
|
||||
)
|
||||
}
|
||||
|
||||
enum time_span_type: {
|
||||
everything: 'everything',
|
||||
monthly: 'monthly'
|
||||
}
|
||||
|
||||
enum status: {
|
||||
pending: 'pending',
|
||||
generated: 'generated'
|
||||
}
|
||||
|
||||
aasm whiny_persistence: true, column: :status, enum: true do
|
||||
state :pending, initial: true
|
||||
state :generated
|
||||
|
||||
event :make_available do
|
||||
transitions from: :pending, to: :generated
|
||||
end
|
||||
end
|
||||
|
||||
def available?
|
||||
status == 'generated' && file.attached?
|
||||
end
|
||||
|
||||
def filename(procedure)
|
||||
if time_span_type == 'everything'
|
||||
"procedure-#{procedure.id}.zip"
|
||||
else
|
||||
"procedure-#{procedure.id}-mois-#{I18n.l(month, format: '%Y-%m')}.zip"
|
||||
end
|
||||
end
|
||||
|
||||
def self.find_or_create_archive(time_span_type, month, groupe_instructeurs)
|
||||
create_with(groupe_instructeurs: groupe_instructeurs)
|
||||
.create_or_find_by(time_span_type: time_span_type, month: month, key: generate_cache_key(groupe_instructeurs))
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def self.generate_cache_key(groupe_instructeurs)
|
||||
groupe_instructeurs.map(&:id).sort.join('-')
|
||||
end
|
||||
end
|
|
@ -172,7 +172,12 @@ class Dossier < ApplicationRecord
|
|||
scope :en_construction, -> { not_archived.state_en_construction }
|
||||
scope :en_instruction, -> { not_archived.state_en_instruction }
|
||||
scope :termine, -> { not_archived.state_termine }
|
||||
scope :downloadable_sorted, -> {
|
||||
scope :processed_in_month, -> (month) do
|
||||
state_termine
|
||||
.joins(:traitements)
|
||||
.where(traitements: { processed_at: month.beginning_of_month..month.end_of_month })
|
||||
end
|
||||
scope :downloadable_sorted, -> {
|
||||
state_not_brouillon
|
||||
.includes(
|
||||
:user,
|
||||
|
|
|
@ -25,6 +25,7 @@ class Instructeur < ApplicationRecord
|
|||
has_many :followed_dossiers, through: :follows, source: :dossier
|
||||
has_many :previously_followed_dossiers, -> { distinct }, through: :previous_follows, source: :dossier
|
||||
has_many :trusted_device_tokens, dependent: :destroy
|
||||
has_many :archives
|
||||
|
||||
has_one :user, dependent: :nullify
|
||||
|
||||
|
|
|
@ -156,6 +156,20 @@ class Procedure < ApplicationRecord
|
|||
includes(:draft_revision, :published_revision, administrateurs: :user)
|
||||
}
|
||||
|
||||
scope :for_download, -> {
|
||||
includes(
|
||||
:groupe_instructeurs,
|
||||
dossiers: {
|
||||
champs: [
|
||||
piece_justificative_file_attachment: :blob,
|
||||
champs: [
|
||||
piece_justificative_file_attachment: :blob
|
||||
]
|
||||
]
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
validates :libelle, presence: true, allow_blank: false, allow_nil: false
|
||||
validates :description, presence: true, allow_blank: false, allow_nil: false
|
||||
validates :administrateurs, presence: true
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
class PiecesJustificativesService
|
||||
def self.liste_pieces_justificatives(dossier)
|
||||
dossier_export = generate_dossier_export(dossier)
|
||||
pjs_champs = pjs_for_champs(dossier)
|
||||
pjs_commentaires = pjs_for_commentaires(dossier)
|
||||
pjs_dossier = pjs_for_dossier(dossier)
|
||||
|
||||
(pjs_champs + pjs_commentaires + pjs_dossier)
|
||||
([dossier_export] + pjs_champs + pjs_commentaires + pjs_dossier)
|
||||
.filter(&:attached?)
|
||||
end
|
||||
|
||||
|
@ -43,6 +44,17 @@ class PiecesJustificativesService
|
|||
|
||||
private
|
||||
|
||||
def self.generate_dossier_export(dossier)
|
||||
pdf = ApplicationController
|
||||
.render(template: 'dossiers/show', formats: [:pdf],
|
||||
assigns: {
|
||||
include_infos_administration: true,
|
||||
dossier: dossier
|
||||
})
|
||||
dossier.pdf_export_for_instructeur.attach(io: StringIO.open(pdf), filename: "export-#{dossier.id}.pdf", content_type: 'application/pdf')
|
||||
dossier.pdf_export_for_instructeur
|
||||
end
|
||||
|
||||
def self.pjs_for_champs(dossier)
|
||||
allowed_champs = dossier.champs + dossier.champs_private
|
||||
|
||||
|
|
72
app/services/procedure_archive_service.rb
Normal file
72
app/services/procedure_archive_service.rb
Normal file
|
@ -0,0 +1,72 @@
|
|||
require 'tempfile'
|
||||
|
||||
class ProcedureArchiveService
|
||||
def initialize(procedure)
|
||||
@procedure = procedure
|
||||
end
|
||||
|
||||
def create_pending_archive(instructeur, type, month = nil)
|
||||
groupe_instructeurs = instructeur
|
||||
.groupe_instructeurs
|
||||
.where(procedure: @procedure)
|
||||
|
||||
Archive.find_or_create_archive(type, month, groupe_instructeurs)
|
||||
end
|
||||
|
||||
def collect_files_archive(archive, instructeur)
|
||||
if archive.time_span_type == 'everything'
|
||||
dossiers = @procedure.dossiers.state_termine
|
||||
else
|
||||
dossiers = @procedure.dossiers.processed_in_month(archive.month)
|
||||
end
|
||||
|
||||
files = create_list_of_attachments(dossiers)
|
||||
|
||||
tmp_file = Tempfile.new(['tc', '.zip'])
|
||||
|
||||
Zip::OutputStream.open(tmp_file) do |zipfile|
|
||||
files.each do |attachment, pj_filename|
|
||||
zipfile.put_next_entry(pj_filename)
|
||||
zipfile.puts(attachment.download)
|
||||
end
|
||||
end
|
||||
|
||||
archive.file.attach(io: File.open(tmp_file), filename: archive.filename(@procedure))
|
||||
tmp_file.delete
|
||||
archive.make_available!
|
||||
InstructeurMailer.send_archive(instructeur, @procedure, archive).deliver_later
|
||||
end
|
||||
|
||||
def self.procedure_files_size(procedure)
|
||||
dossiers_files_size(procedure.dossiers)
|
||||
end
|
||||
|
||||
def self.dossiers_files_size(dossiers)
|
||||
dossiers.map do |dossier|
|
||||
liste_pieces_justificatives_for_archive(dossier).sum(&:byte_size)
|
||||
end.sum
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def create_list_of_attachments(dossiers)
|
||||
dossiers.flat_map do |dossier|
|
||||
ActiveStorage::DownloadableFile.create_list_from_dossier(dossier)
|
||||
end
|
||||
end
|
||||
|
||||
def self.attachments_from_champs_piece_justificative(champs)
|
||||
champs
|
||||
.filter { |c| c.type_champ == TypeDeChamp.type_champs.fetch(:piece_justificative) }
|
||||
.filter { |pj| pj.piece_justificative_file.attached? }
|
||||
.map(&:piece_justificative_file)
|
||||
end
|
||||
|
||||
def self.liste_pieces_justificatives_for_archive(dossier)
|
||||
champs_blocs_repetables = dossier.champs
|
||||
.filter { |c| c.type_champ == TypeDeChamp.type_champs.fetch(:repetition) }
|
||||
.flat_map(&:champs)
|
||||
|
||||
attachments_from_champs_piece_justificative(champs_blocs_repetables + dossier.champs)
|
||||
end
|
||||
end
|
21
app/views/instructeur_mailer/send_archive.html.haml
Normal file
21
app/views/instructeur_mailer/send_archive.html.haml
Normal file
|
@ -0,0 +1,21 @@
|
|||
- content_for(:title, 'Votre archive est disponible')
|
||||
|
||||
%p
|
||||
Bonjour,
|
||||
|
||||
%p
|
||||
Votre archive pour la démarche
|
||||
= link_to("#{@procedure.id} − #{@procedure.libelle}", instructeur_procedure_url(@procedure.id))
|
||||
est disponible.
|
||||
Vous pouvez la télécharger dans votre espace de gestion des archives.
|
||||
|
||||
%p
|
||||
= round_button('Consulter mes archives', instructeur_archives_url(@procedure), :primary)
|
||||
|
||||
%p
|
||||
Ce fichier est
|
||||
%b valide une semaine
|
||||
et peut-être téléchargé
|
||||
%b plusieurs fois.
|
||||
|
||||
= render partial: "layouts/mailers/signature"
|
1
app/views/instructeurs/archives/create.js.haml
Normal file
1
app/views/instructeurs/archives/create.js.haml
Normal file
|
@ -0,0 +1 @@
|
|||
= render_flash(sticky: true)
|
91
app/views/instructeurs/archives/index.html.haml
Normal file
91
app/views/instructeurs/archives/index.html.haml
Normal file
|
@ -0,0 +1,91 @@
|
|||
- content_for(:title, "Archives pour #{@procedure.libelle}")
|
||||
|
||||
= render partial: 'new_administrateur/breadcrumbs',
|
||||
locals: { steps: [link_to(@procedure.libelle, instructeur_procedure_path(@procedure)),
|
||||
'Archives'] }
|
||||
|
||||
.container
|
||||
%h1 Archives
|
||||
|
||||
.card.featured
|
||||
.card-title Gestion de vos archives
|
||||
%p
|
||||
Vous pouvez télécharger les archives des dossiers terminés depuis la publication de la procédure au format Zip.
|
||||
|
||||
%p
|
||||
Cet export contient les demande déposée par l'usager et la liste des pièces justificatives transmises.
|
||||
|
||||
%p
|
||||
Cet export n'est pas possible pour le moment pour les démarches à forte volumétrie.
|
||||
Nous vous invitons à regarder
|
||||
= link_to 'la documentation', ARCHIVAGE_DOC_URL
|
||||
afin de voir les options à votre disposition pour mettre en place un système d'archive.
|
||||
|
||||
%table.table.hoverable
|
||||
%thead
|
||||
%tr
|
||||
%th
|
||||
%th Nombre de dossiers terminés
|
||||
%th Poids estimé
|
||||
%th Télécharger
|
||||
|
||||
%tbody
|
||||
- if can_generate_archive?(@dossiers_termines, @poids_total)
|
||||
%tr
|
||||
- matching_archive = @archives.find_by(time_span_type: 'everything')
|
||||
%td
|
||||
Tous les dossiers
|
||||
%td
|
||||
= @dossiers_termines.count
|
||||
%td
|
||||
- if matching_archive.present? && matching_archive.available?
|
||||
- weight = matching_archive.file.byte_size
|
||||
- else
|
||||
- weight = @poids_total
|
||||
= number_to_human_size(weight)
|
||||
%td
|
||||
- if matching_archive.try(&:available?)
|
||||
= link_to url_for(matching_archive.file), class: 'button primary' do
|
||||
%span.icon.download-white
|
||||
= t(:archive_ready_html, generated_period: time_ago_in_words(matching_archive.updated_at), scope: [:instructeurs, :procedure])
|
||||
- elsif matching_archive.try(&:pending?)
|
||||
%span.icon.retry
|
||||
= t(:archive_pending_html, created_period: time_ago_in_words(matching_archive.created_at), scope: [:instructeurs, :procedure])
|
||||
- elsif @dossiers_termines.count > 0
|
||||
= link_to instructeur_archives_path(@procedure, type: 'everything'), method: :post, class: "button" do
|
||||
%span.icon.new-folder
|
||||
Demander la création
|
||||
- else
|
||||
Rien à télécharger !
|
||||
- @archivable_months.each do |month|
|
||||
- dossiers_termines = @procedure.dossiers.processed_in_month(month)
|
||||
- nb_dossiers_termines = dossiers_termines.count
|
||||
- matching_archive = @archives.find_by(time_span_type: 'monthly', month: month)
|
||||
%tr
|
||||
%td
|
||||
= I18n.l(month, format: "%B %Y")
|
||||
%td
|
||||
= nb_dossiers_termines
|
||||
%td
|
||||
- if matching_archive.present? && matching_archive.available?
|
||||
- weight = matching_archive.file.byte_size
|
||||
- else
|
||||
- weight = ProcedureArchiveService::dossiers_files_size(dossiers_termines)
|
||||
= number_to_human_size(weight)
|
||||
%td
|
||||
- if nb_dossiers_termines > 0
|
||||
- if matching_archive.present?
|
||||
- if matching_archive.status == 'generated' && matching_archive.file.attached?
|
||||
= link_to url_for(matching_archive.file), class: 'button primary' do
|
||||
%span.icon.download-white
|
||||
= t(:archive_ready_html, generated_period: time_ago_in_words(matching_archive.updated_at), scope: [:instructeurs, :procedure])
|
||||
- else
|
||||
%span.icon.retry
|
||||
= t(:archive_pending_html, created_period: time_ago_in_words(matching_archive.created_at), scope: [:instructeurs, :procedure])
|
||||
- else
|
||||
= link_to instructeur_archives_path(@procedure, type:'monthly', month: month.strftime('%Y-%m')), method: :post, class: "button" do
|
||||
%span.icon.new-folder
|
||||
Démander la création
|
||||
- else
|
||||
Rien à télécharger !
|
||||
|
|
@ -16,3 +16,6 @@
|
|||
- else
|
||||
%span{ 'data-export-poll-url': download_export_instructeur_procedure_path(procedure, export_format: format, no_progress_notification: true) }
|
||||
= t(:export_pending_html, export_time: time_ago_in_words(export.created_at), export_format: ".#{format}", scope: [:instructeurs, :procedure])
|
||||
- if procedure.feature_enabled?(:archive_zip_globale)
|
||||
%li
|
||||
= link_to t(:download_archive, scope: [:instructeurs, :procedure]), instructeur_archives_path(procedure)
|
||||
|
|
|
@ -121,7 +121,7 @@
|
|||
%button.button.dropdown-button{ 'aria-expanded' => 'false', 'aria-controls' => 'custom-menu' }
|
||||
Personnaliser
|
||||
#custom-menu.dropdown-content.fade-in-down
|
||||
= form_tag update_displayed_fields_instructeur_procedure_path(@procedure), method: :patch, class: 'dropdown-form columns-form' do
|
||||
= form_tag update_displayed_fields_instructeur_procedure_path(@procedure), method: :patch, class: 'dropdown-form large columns-form' do
|
||||
- hidden_field_id = SecureRandom.uuid
|
||||
= hidden_field_tag :values, nil, data: { uuid: hidden_field_id }
|
||||
= react_component("ComboMultipleDropdownList", options: @displayed_fields_options, selected: @displayed_fields_selected, disabled: [], hiddenFieldId: hidden_field_id, label: 'colonne')
|
||||
|
|
|
@ -8,3 +8,6 @@ fr:
|
|||
ods_html: Demander un export au format .ods
|
||||
export_ready_html: Télécharger l’export au format %{export_format}<br>(généré il y a %{export_time})
|
||||
export_pending_html: Un export au format %{export_format} est en train d’être généré<br>(demandé il y a %{export_time})
|
||||
download_archive: Télécharger une archive au format .zip de tous les dossiers et leurs pièces jointes
|
||||
archive_pending_html: Archive en cours de création<br>(demandée il y a %{created_period})
|
||||
archive_ready_html: Télécharger l'archive<br>(demandée il y a %{generated_period})
|
||||
|
|
|
@ -383,6 +383,8 @@ Rails.application.routes.draw do
|
|||
get 'telecharger_pjs' => 'dossiers#telecharger_pjs'
|
||||
end
|
||||
end
|
||||
|
||||
resources :archives, only: [:index, :create, :show], controller: 'archives'
|
||||
end
|
||||
end
|
||||
get "recherche" => "recherche#index"
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
class CreateArchiveForGroupeInstructeur < ActiveRecord::Migration[6.0]
|
||||
def change
|
||||
create_table :archives do |t|
|
||||
t.string :status, null: false
|
||||
t.date :month
|
||||
t.string :content_type, null: false
|
||||
t.timestamps
|
||||
end
|
||||
|
||||
create_table "archives_groupe_instructeurs", force: :cascade do |t|
|
||||
t.belongs_to :archive, foreign_key: true, null: false
|
||||
t.belongs_to :groupe_instructeur, foreign_key: true, null: false
|
||||
|
||||
t.timestamps
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,5 @@
|
|||
class RenameContentTypeToToTimeSpanTypeForArchives < ActiveRecord::Migration[6.1]
|
||||
def change
|
||||
rename_column :archives, :content_type, :time_span_type
|
||||
end
|
||||
end
|
6
db/migrate/20210427124500_add_key_to_archives.rb
Normal file
6
db/migrate/20210427124500_add_key_to_archives.rb
Normal file
|
@ -0,0 +1,6 @@
|
|||
class AddKeyToArchives < ActiveRecord::Migration[6.1]
|
||||
def change
|
||||
add_column :archives, :key, :text, null: false
|
||||
add_index :archives, [:key, :time_span_type, :month], unique: true
|
||||
end
|
||||
end
|
23
db/schema.rb
23
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: 2021_04_27_120002) do
|
||||
ActiveRecord::Schema.define(version: 2021_04_27_124500) do
|
||||
|
||||
# These are extensions that must be enabled in order to support this database
|
||||
enable_extension "plpgsql"
|
||||
|
@ -81,6 +81,25 @@ ActiveRecord::Schema.define(version: 2021_04_27_120002) do
|
|||
t.index ["procedure_id"], name: "index_administrateurs_procedures_on_procedure_id"
|
||||
end
|
||||
|
||||
create_table "archives", force: :cascade do |t|
|
||||
t.string "status", null: false
|
||||
t.date "month"
|
||||
t.string "time_span_type", null: false
|
||||
t.datetime "created_at", precision: 6, null: false
|
||||
t.datetime "updated_at", precision: 6, null: false
|
||||
t.text "key", null: false
|
||||
t.index ["key", "time_span_type", "month"], name: "index_archives_on_key_and_time_span_type_and_month", unique: true
|
||||
end
|
||||
|
||||
create_table "archives_groupe_instructeurs", force: :cascade do |t|
|
||||
t.bigint "archive_id", null: false
|
||||
t.bigint "groupe_instructeur_id", null: false
|
||||
t.datetime "created_at", precision: 6, null: false
|
||||
t.datetime "updated_at", precision: 6, null: false
|
||||
t.index ["archive_id"], name: "index_archives_groupe_instructeurs_on_archive_id"
|
||||
t.index ["groupe_instructeur_id"], name: "index_archives_groupe_instructeurs_on_groupe_instructeur_id"
|
||||
end
|
||||
|
||||
create_table "assign_tos", id: :serial, force: :cascade do |t|
|
||||
t.integer "instructeur_id"
|
||||
t.datetime "created_at"
|
||||
|
@ -735,6 +754,8 @@ ActiveRecord::Schema.define(version: 2021_04_27_120002) do
|
|||
end
|
||||
|
||||
add_foreign_key "active_storage_variant_records", "active_storage_blobs", column: "blob_id"
|
||||
add_foreign_key "archives_groupe_instructeurs", "archives"
|
||||
add_foreign_key "archives_groupe_instructeurs", "groupe_instructeurs"
|
||||
add_foreign_key "assign_tos", "groupe_instructeurs"
|
||||
add_foreign_key "attestation_templates", "procedures"
|
||||
add_foreign_key "attestations", "dossiers"
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
"@fortawesome/fontawesome-svg-core": "^1.2.34",
|
||||
"@fortawesome/free-solid-svg-icons": "^5.15.2",
|
||||
"@fortawesome/react-fontawesome": "^0.1.14",
|
||||
"@heroicons/react": "^1.0.1",
|
||||
"@mapbox/mapbox-gl-draw": "^1.2.0",
|
||||
"@rails/actiontext": "^6.0.3",
|
||||
"@rails/activestorage": "^6.0.3",
|
||||
|
|
56
spec/controllers/instructeurs/archives_controller_spec.rb
Normal file
56
spec/controllers/instructeurs/archives_controller_spec.rb
Normal file
|
@ -0,0 +1,56 @@
|
|||
describe Instructeurs::ArchivesController, type: :controller do
|
||||
let(:procedure1) { create(:procedure, :published, groupe_instructeurs: [gi1]) }
|
||||
let(:procedure2) { create(:procedure, :published, groupe_instructeurs: [gi2]) }
|
||||
let!(:instructeur) { create(:instructeur, groupe_instructeurs: [gi1, gi2]) }
|
||||
let!(:archive1) { create(:archive, :generated, groupe_instructeurs: [gi1]) }
|
||||
let!(:archive2) { create(:archive, :generated, groupe_instructeurs: [gi2]) }
|
||||
let(:gi1) { create(:groupe_instructeur) }
|
||||
let(:gi2) { create(:groupe_instructeur) }
|
||||
|
||||
before do
|
||||
sign_in(instructeur.user)
|
||||
Flipper.enable(:archive_zip_globale, procedure1)
|
||||
end
|
||||
|
||||
after { Timecop.return }
|
||||
|
||||
describe '#index' do
|
||||
before do
|
||||
create_dossier_for_month(procedure1, 2021, 3)
|
||||
create_dossier_for_month(procedure1, 2021, 3)
|
||||
create_dossier_for_month(procedure1, 2021, 2)
|
||||
Timecop.freeze(Time.zone.local(2021, 3, 5))
|
||||
end
|
||||
|
||||
it 'displays archives' do
|
||||
get :index, { params: { procedure_id: procedure1.id } }
|
||||
|
||||
expect(assigns(:dossiers_termines).size).to eq(3)
|
||||
expect(assigns(:archives)).to eq([archive1])
|
||||
end
|
||||
end
|
||||
|
||||
describe '#create' do
|
||||
let(:month) { '21-03' }
|
||||
let(:date_month) { Date.strptime(month, "%Y-%m") }
|
||||
let(:archive) { create(:archive) }
|
||||
let(:subject) do
|
||||
post :create, {
|
||||
params: { procedure_id: procedure1.id, type: 'monthly', month: month }
|
||||
}
|
||||
end
|
||||
|
||||
it "performs archive creation job" do
|
||||
allow_any_instance_of(ProcedureArchiveService).to receive(:create_pending_archive).and_return(archive)
|
||||
expect { subject }.to have_enqueued_job(ArchiveCreationJob).with(procedure1, archive, instructeur)
|
||||
expect(flash.notice).to include("Votre demande a été prise en compte")
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def create_dossier_for_month(procedure, year, month)
|
||||
Timecop.freeze(Time.zone.local(year, month, 5))
|
||||
create(:dossier, :accepte, :with_attestation, procedure: procedure)
|
||||
end
|
||||
end
|
|
@ -712,13 +712,6 @@ describe Instructeurs::DossiersController, type: :controller do
|
|||
dossier_id: dossier.id
|
||||
}
|
||||
end
|
||||
|
||||
context 'when zip download is disabled through flipflop' do
|
||||
it 'is forbidden' do
|
||||
subject
|
||||
expect(response).to have_http_status(:forbidden)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "#delete_dossier" do
|
||||
|
|
15
spec/factories/archive.rb
Normal file
15
spec/factories/archive.rb
Normal file
|
@ -0,0 +1,15 @@
|
|||
FactoryBot.define do
|
||||
factory :archive do
|
||||
time_span_type { 'everything' }
|
||||
groupe_instructeurs { [association(:groupe_instructeur)] }
|
||||
key { 'unique-key' }
|
||||
|
||||
trait :pending do
|
||||
status { 'pending' }
|
||||
end
|
||||
|
||||
trait :generated do
|
||||
status { 'generated' }
|
||||
end
|
||||
end
|
||||
end
|
|
@ -12,6 +12,8 @@ feature 'Inviting an expert:', js: true do
|
|||
|
||||
context 'as an Instructeur' do
|
||||
scenario 'I can invite an expert' do
|
||||
allow(ClamavService).to receive(:safe_file?).and_return(true)
|
||||
|
||||
# assign instructeur to linked dossier
|
||||
instructeur.assign_to_procedure(linked_dossier.procedure)
|
||||
|
||||
|
|
|
@ -130,8 +130,8 @@ feature 'Instructing a dossier:', js: true do
|
|||
|
||||
click_on 'Personnes impliquées'
|
||||
|
||||
select_multi('email instructeur', instructeur_2.email)
|
||||
select_multi('email instructeur', instructeur_3.email)
|
||||
select_multi_combobox('email instructeur', instructeur_2.email, instructeur_2.id)
|
||||
select_multi_combobox('email instructeur', instructeur_3.email, instructeur_3.id)
|
||||
|
||||
click_on 'Envoyer'
|
||||
|
||||
|
@ -165,10 +165,10 @@ feature 'Instructing a dossier:', js: true do
|
|||
|
||||
expect(DownloadHelpers.download).to include "dossier-#{dossier.id}.zip"
|
||||
expect(files.size).to be 3
|
||||
expect(files[0].filename.include?('piece_justificative_0')).to be_truthy
|
||||
expect(files[0].uncompressed_size).to be File.size(path)
|
||||
expect(files[1].filename.include?('horodatage/operation')).to be_truthy
|
||||
expect(files[2].filename.include?('dossier/export')).to be_truthy
|
||||
expect(files[0].filename.include?('export')).to be_truthy
|
||||
expect(files[1].filename.include?('piece_justificative_0')).to be_truthy
|
||||
expect(files[1].uncompressed_size).to be File.size(path)
|
||||
expect(files[2].filename.include?('horodatage/operation')).to be_truthy
|
||||
end
|
||||
|
||||
scenario 'A instructeur can download an archive containing several identical attachments' do
|
||||
|
@ -180,13 +180,13 @@ feature 'Instructing a dossier:', js: true do
|
|||
|
||||
expect(DownloadHelpers.download).to include "dossier-#{dossier.id}.zip"
|
||||
expect(files.size).to be 4
|
||||
expect(files[0].filename.include?('piece_justificative_0')).to be_truthy
|
||||
expect(files[0].filename.include?('export')).to be_truthy
|
||||
expect(files[1].filename.include?('piece_justificative_0')).to be_truthy
|
||||
expect(files[0].filename).not_to eq files[1].filename
|
||||
expect(files[0].uncompressed_size).to be File.size(path)
|
||||
expect(files[2].filename.include?('piece_justificative_0')).to be_truthy
|
||||
expect(files[1].filename).not_to eq files[2].filename
|
||||
expect(files[1].uncompressed_size).to be File.size(path)
|
||||
expect(files[2].filename.include?('horodatage/operation')).to be_truthy
|
||||
expect(files[3].filename.include?('dossier/export')).to be_truthy
|
||||
expect(files[2].uncompressed_size).to be File.size(path)
|
||||
expect(files[3].filename.include?('horodatage/operation')).to be_truthy
|
||||
end
|
||||
|
||||
before { DownloadHelpers.clear_downloads }
|
||||
|
|
|
@ -32,7 +32,7 @@ feature "procedure filters" do
|
|||
end
|
||||
|
||||
scenario "should add be able to add created_at column", js: true do
|
||||
add_column("Créé le")
|
||||
add_column("Créé le", "self/created_at")
|
||||
within ".dossiers-table" do
|
||||
expect(page).to have_link("Créé le")
|
||||
expect(page).to have_link(new_unfollow_dossier.created_at.strftime('%d/%m/%Y'))
|
||||
|
@ -40,7 +40,7 @@ feature "procedure filters" do
|
|||
end
|
||||
|
||||
scenario "should add be able to add and remove custom type_de_champ column", js: true do
|
||||
add_column(type_de_champ.libelle)
|
||||
add_column(type_de_champ.libelle, "type_de_champ/#{type_de_champ.stable_id}")
|
||||
within ".dossiers-table" do
|
||||
expect(page).to have_link(type_de_champ.libelle)
|
||||
expect(page).to have_link(champ.value)
|
||||
|
@ -123,15 +123,16 @@ feature "procedure filters" do
|
|||
click_button "Ajouter le filtre"
|
||||
end
|
||||
|
||||
def add_column(column_name)
|
||||
def add_column(column_name, column_path)
|
||||
click_on 'Personnaliser'
|
||||
select_multi('colonne', column_name)
|
||||
select_multi_combobox('colonne', column_name, column_path)
|
||||
click_button "Enregistrer"
|
||||
end
|
||||
|
||||
def remove_column(column_name)
|
||||
click_on 'Personnaliser'
|
||||
find(:xpath, "//li[contains(text(), '#{column_name}')]/span[contains(text(), 'x')]").click
|
||||
find(:xpath, ".//li[contains(text(), \"#{column_name}\")]/button", text: 'Désélectionner').click
|
||||
find("body").native.send_key("Escape")
|
||||
click_button "Enregistrer"
|
||||
end
|
||||
end
|
||||
|
|
|
@ -25,16 +25,13 @@ feature 'The user' do
|
|||
check('val1')
|
||||
check('val3')
|
||||
select('bravo', from: form_id_for('simple_choice_drop_down_list_long'))
|
||||
select_multi('multiple_choice_drop_down_list_long', 'alpha')
|
||||
select_multi('multiple_choice_drop_down_list_long', 'charly')
|
||||
select_multi_combobox('multiple_choice_drop_down_list_long', 'alp', 'alpha')
|
||||
select_multi_combobox('multiple_choice_drop_down_list_long', 'cha', 'charly')
|
||||
|
||||
select_champ_geo('pays', 'aust', 'AUSTRALIE')
|
||||
|
||||
select_champ_geo('regions', 'Ma', 'Martinique')
|
||||
|
||||
select_champ_geo('departements', 'Ai', '02 - Aisne')
|
||||
|
||||
select_champ_geo('communes', 'Ambl', 'Ambléon (01300)')
|
||||
select_combobox('pays', 'aust', 'AUSTRALIE')
|
||||
select_combobox('regions', 'Ma', 'Martinique')
|
||||
select_combobox('departements', 'Ai', '02 - Aisne')
|
||||
select_combobox('communes', 'Ambl', 'Ambléon (01300)')
|
||||
|
||||
check('engagement')
|
||||
fill_in('dossier_link', with: '123')
|
||||
|
@ -320,10 +317,6 @@ feature 'The user' do
|
|||
e.sibling('.datetime').first('select')[:id][0..-4]
|
||||
end
|
||||
|
||||
def have_hidden_field(libelle, with:)
|
||||
have_css("##{form_id_for(libelle)}[value=\"#{with}\"]")
|
||||
end
|
||||
|
||||
def champ_value_for(libelle)
|
||||
champs = user_dossier.champs
|
||||
champs.find { |c| c.libelle == libelle }.value
|
||||
|
@ -352,14 +345,4 @@ feature 'The user' do
|
|||
expect(page).to have_selected_value("#{field}_4i", selected: date.strftime('%H'))
|
||||
expect(page).to have_selected_value("#{field}_5i", selected: date.strftime('%M'))
|
||||
end
|
||||
|
||||
def select_champ_geo(champ, fill_with, value)
|
||||
input = find("input[aria-label=#{champ}")
|
||||
input.click
|
||||
input.fill_in with: fill_with
|
||||
selector = "li[data-option-value=\"#{value}\"]"
|
||||
find(selector).click
|
||||
expect(page).to have_css(selector)
|
||||
expect(page).to have_hidden_field(champ, with: value)
|
||||
end
|
||||
end
|
||||
|
|
51
spec/models/archive_spec.rb
Normal file
51
spec/models/archive_spec.rb
Normal file
|
@ -0,0 +1,51 @@
|
|||
describe Dossier do
|
||||
include ActiveJob::TestHelper
|
||||
|
||||
before { Timecop.freeze(Time.zone.now) }
|
||||
after { Timecop.return }
|
||||
|
||||
let(:archive) { create(:archive) }
|
||||
|
||||
describe 'scopes' do
|
||||
describe 'staled' do
|
||||
let(:recent_archive) { create(:archive) }
|
||||
let(:staled_archive) { create(:archive, updated_at: (Archive::RETENTION_DURATION + 2).days.ago) }
|
||||
|
||||
subject do
|
||||
archive; recent_archive; staled_archive
|
||||
Archive.stale
|
||||
end
|
||||
|
||||
it { is_expected.to match_array([staled_archive]) }
|
||||
end
|
||||
end
|
||||
|
||||
describe '.status' do
|
||||
it { expect(archive.status).to eq('pending') }
|
||||
end
|
||||
|
||||
describe '#make_available!' do
|
||||
before { archive.make_available! }
|
||||
it { expect(archive.status).to eq('generated') }
|
||||
end
|
||||
|
||||
describe '#available?' do
|
||||
subject { archive.available? }
|
||||
context 'without attachment' do
|
||||
let(:archive) { create(:archive, file: nil) }
|
||||
it { is_expected.to eq(false) }
|
||||
end
|
||||
|
||||
context 'with an attachment' do
|
||||
context 'when the attachment was created but the process was not over' do
|
||||
let(:archive) { create(:archive, :pending, file: Rack::Test::UploadedFile.new('spec/fixtures/files/file.pdf', 'application/pdf')) }
|
||||
it { is_expected.to eq(false) }
|
||||
end
|
||||
|
||||
context 'when the attachment was created but the process was not over' do
|
||||
let(:archive) { create(:archive, :generated, file: Rack::Test::UploadedFile.new('spec/fixtures/files/file.pdf', 'application/pdf')) }
|
||||
it { is_expected.to eq(true) }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -16,7 +16,12 @@ describe PiecesJustificativesService do
|
|||
# to be exported
|
||||
it 'ensures no titre identite is given' do
|
||||
expect(champ_identite.piece_justificative_file).to be_attached
|
||||
expect(subject).to eq([])
|
||||
expect(subject.any? { |piece| piece.name == 'piece_justificative_file' }).to be_falsy
|
||||
end
|
||||
|
||||
it 'returns export pdf of the dossier' do
|
||||
expect(champ_identite.piece_justificative_file).to be_attached
|
||||
expect(subject.any? { |piece| piece.name == 'pdf_export_for_instructeur' }).to be_truthy
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
83
spec/services/procedure_archive_service_spec.rb
Normal file
83
spec/services/procedure_archive_service_spec.rb
Normal file
|
@ -0,0 +1,83 @@
|
|||
describe ProcedureArchiveService do
|
||||
let(:procedure) { create(:procedure, :published) }
|
||||
let(:instructeur) { create(:instructeur) }
|
||||
let(:service) { ProcedureArchiveService.new(procedure) }
|
||||
let(:year) { 2020 }
|
||||
let(:month) { 3 }
|
||||
let(:date_month) { Date.strptime("#{year}-#{month}", "%Y-%m") }
|
||||
describe '#create_pending_archive' do
|
||||
context 'for a specific month' do
|
||||
it 'creates a pending archive' do
|
||||
archive = service.create_pending_archive(instructeur, 'monthly', date_month)
|
||||
|
||||
expect(archive.time_span_type).to eq 'monthly'
|
||||
expect(archive.month).to eq date_month
|
||||
expect(archive.pending?).to be_truthy
|
||||
end
|
||||
end
|
||||
|
||||
context 'for all months' do
|
||||
it 'creates a pending archive' do
|
||||
archive = service.create_pending_archive(instructeur, 'everything')
|
||||
|
||||
expect(archive.time_span_type).to eq 'everything'
|
||||
expect(archive.month).to eq nil
|
||||
expect(archive.pending?).to be_truthy
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#collect_files_archive' do
|
||||
before do
|
||||
create_dossier_for_month(year, month)
|
||||
create_dossier_for_month(2020, month)
|
||||
end
|
||||
|
||||
after { Timecop.return }
|
||||
|
||||
context 'for a specific month' do
|
||||
let(:archive) { create(:archive, time_span_type: 'monthly', status: 'pending', month: date_month) }
|
||||
let(:year) { 2021 }
|
||||
let(:mailer) { double('mailer', deliver_later: true) }
|
||||
|
||||
it 'collect files' do
|
||||
expect(InstructeurMailer).to receive(:send_archive).and_return(mailer)
|
||||
|
||||
service.collect_files_archive(archive, instructeur)
|
||||
|
||||
archive.file.open do |f|
|
||||
files = ZipTricks::FileReader.read_zip_structure(io: f)
|
||||
expect(files.size).to be 2
|
||||
expect(files.first.filename).to include("export")
|
||||
expect(files.last.filename).to include("attestation")
|
||||
end
|
||||
expect(archive.file.attached?).to be_truthy
|
||||
end
|
||||
end
|
||||
|
||||
context 'for all months' do
|
||||
let(:archive) { create(:archive, time_span_type: 'everything', status: 'pending') }
|
||||
let(:mailer) { double('mailer', deliver_later: true) }
|
||||
|
||||
it 'collect files' do
|
||||
expect(InstructeurMailer).to receive(:send_archive).and_return(mailer)
|
||||
|
||||
service.collect_files_archive(archive, instructeur)
|
||||
|
||||
archive = Archive.last
|
||||
archive.file.open do |f|
|
||||
files = ZipTricks::FileReader.read_zip_structure(io: f)
|
||||
expect(files.size).to be 4
|
||||
end
|
||||
expect(archive.file.attached?).to be_truthy
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def create_dossier_for_month(year, month)
|
||||
Timecop.freeze(Time.zone.local(year, month, 5))
|
||||
create(:dossier, :accepte, :with_attestation, procedure: procedure)
|
||||
end
|
||||
end
|
|
@ -103,23 +103,43 @@ module FeatureHelpers
|
|||
end
|
||||
end
|
||||
|
||||
def select_multi(champ, with)
|
||||
input = find("input[aria-label='#{champ}'")
|
||||
def select_combobox(champ, fill_with, value)
|
||||
input = find("input[aria-label=\"#{champ}\"")
|
||||
input.click
|
||||
input.fill_in with: fill_with
|
||||
selector = "li[data-option-value=\"#{value}\"]"
|
||||
find(selector).click
|
||||
expect(page).to have_css(selector)
|
||||
expect(page).to have_hidden_field(champ, with: value)
|
||||
end
|
||||
|
||||
# hack because for unknown reason, the click on input doesn't show combobox-popover with selenium driver
|
||||
script = "document.evaluate(\"//input[@aria-label='#{champ}']//ancestor::div[@data-reach-combobox]/div[@data-reach-combobox-popover]\", document, null, XPathResult.UNORDERED_NODE_ITERATOR_TYPE, null).iterateNext().removeAttribute(\"hidden\")"
|
||||
execute_script(script)
|
||||
|
||||
element = find(:xpath, "//input[@aria-label='#{champ}']/ancestor::div[@data-reach-combobox]//div[@data-reach-combobox-popover]//li/span[normalize-space(text())='#{with}']")
|
||||
element.click
|
||||
def select_multi_combobox(champ, fill_with, value)
|
||||
input = find("input[aria-label=\"#{champ}\"")
|
||||
input.click
|
||||
input.fill_in with: fill_with
|
||||
selector = "li[data-option-value=\"#{value}\"]"
|
||||
find(selector).click
|
||||
check_selected_value(champ, value)
|
||||
end
|
||||
|
||||
def check_selected_values(champ, values)
|
||||
combobox = find(:xpath, "//input[@aria-label='#{champ}']/ancestor::div[@data-react-class='ComboMultipleDropdownList']")
|
||||
hiddenFieldId = JSON.parse(combobox["data-react-props"])["hiddenFieldId"]
|
||||
hiddenField = find("input[data-uuid='#{hiddenFieldId}']")
|
||||
expect(values.sort).to eq(JSON.parse(hiddenField.value).sort)
|
||||
combobox = find(:xpath, "//input[@aria-label=\"#{champ}\"]/ancestor::div[@data-react-class='ComboMultipleDropdownList']")
|
||||
hidden_field_id = JSON.parse(combobox["data-react-props"])["hiddenFieldId"]
|
||||
hidden_field = find("input[data-uuid=\"#{hidden_field_id}\"]")
|
||||
hidden_field_values = JSON.parse(hidden_field.value)
|
||||
expect(values.sort).to eq(hidden_field_values.sort)
|
||||
end
|
||||
|
||||
def check_selected_value(champ, value)
|
||||
combobox = find(:xpath, "//input[@aria-label=\"#{champ}\"]/ancestor::div[@data-react-class='ComboMultipleDropdownList']")
|
||||
hidden_field_id = JSON.parse(combobox["data-react-props"])["hiddenFieldId"]
|
||||
hidden_field = find("input[data-uuid=\"#{hidden_field_id}\"]")
|
||||
hidden_field_values = JSON.parse(hidden_field.value)
|
||||
expect(hidden_field_values).to include(value)
|
||||
end
|
||||
|
||||
def have_hidden_field(libelle, with:)
|
||||
have_css("##{form_id_for(libelle)}[value=\"#{with}\"]")
|
||||
end
|
||||
|
||||
# Keep the brower window open after a test success of failure, to
|
||||
|
|
|
@ -16,5 +16,15 @@ describe 'instructeurs/procedures/_download_dossiers.html.haml', type: :view do
|
|||
context "when procedure has at least 1 dossier en construction" do
|
||||
let!(:dossier) { create(:dossier, :en_construction, procedure: procedure) }
|
||||
it { is_expected.to include("Télécharger tous les dossiers") }
|
||||
|
||||
context "With zip archive enabled" do
|
||||
before { Flipper.enable(:archive_zip_globale, procedure) }
|
||||
it { is_expected.to include("Télécharger une archive au format .zip") }
|
||||
end
|
||||
|
||||
context "With zip archive disabled" do
|
||||
before { Flipper.disable(:archive_zip_globale, procedure) }
|
||||
it { is_expected.not_to include("Télécharger une archive au format .zip") }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1031,6 +1031,11 @@
|
|||
dependencies:
|
||||
prop-types "^15.7.2"
|
||||
|
||||
"@heroicons/react@^1.0.1":
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@heroicons/react/-/react-1.0.1.tgz#66d25f6441920bd5c2146ea27fd33995885452dd"
|
||||
integrity sha512-uikw2gKCmqnvjVxitecWfFLMOKyL9BTFcU4VM3hHj9OMwpkCr5Ke+MRMyY2/aQVmsYs4VTq7NCFX05MYwAHi3g==
|
||||
|
||||
"@jest/types@^24.9.0":
|
||||
version "24.9.0"
|
||||
resolved "https://registry.yarnpkg.com/@jest/types/-/types-24.9.0.tgz#63cb26cb7500d069e5a389441a7c6ab5e909fc59"
|
||||
|
|
Loading…
Reference in a new issue