commit
4b43f6a0d3
48 changed files with 125 additions and 750 deletions
|
@ -130,6 +130,11 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.icon-size {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
.state-button {
|
.state-button {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,55 +0,0 @@
|
||||||
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,6 +213,7 @@ module Instructeurs
|
||||||
def telecharger_pjs
|
def telecharger_pjs
|
||||||
return head(:forbidden) if !dossier.attachments_downloadable?
|
return head(:forbidden) if !dossier.attachments_downloadable?
|
||||||
|
|
||||||
|
generate_pdf_for_instructeur_export
|
||||||
files = ActiveStorage::DownloadableFile.create_list_from_dossier(dossier)
|
files = ActiveStorage::DownloadableFile.create_list_from_dossier(dossier)
|
||||||
|
|
||||||
zipline(files, "dossier-#{dossier.id}.zip")
|
zipline(files, "dossier-#{dossier.id}.zip")
|
||||||
|
@ -238,6 +239,12 @@ module Instructeurs
|
||||||
.find(params[:dossier_id])
|
.find(params[:dossier_id])
|
||||||
end
|
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
|
def commentaire_params
|
||||||
params.require(:commentaire).permit(:body, :piece_jointe)
|
params.require(:commentaire).permit(:body, :piece_jointe)
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,5 +0,0 @@
|
||||||
module ArchiveHelper
|
|
||||||
def can_generate_archive?(dossiers_termines, poids_total)
|
|
||||||
dossiers_termines.count < 100 && poids_total < 1.gigabyte
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -241,7 +241,7 @@ function ComboboxToken({ value, ...props }) {
|
||||||
onRemove(value);
|
onRemove(value);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<XIcon style={{ width: '15px', height: '15px' }} />
|
<XIcon className="icon-size" />
|
||||||
<span className="screen-reader-text">Désélectionner</span>
|
<span className="screen-reader-text">Désélectionner</span>
|
||||||
</button>
|
</button>
|
||||||
{value}
|
{value}
|
||||||
|
|
|
@ -1,16 +1,20 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
import { ArrowDownIcon, ArrowUpIcon } from '@heroicons/react/solid';
|
||||||
|
|
||||||
function MoveButton({ isEnabled, icon, title, onClick }) {
|
function MoveButton({ isEnabled, icon, title, onClick }) {
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
className="button small icon-only move"
|
className="button small move"
|
||||||
disabled={!isEnabled}
|
disabled={!isEnabled}
|
||||||
title={title}
|
title={title}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon icon={icon} />
|
{icon == 'arrow-up' ? (
|
||||||
|
<ArrowUpIcon className="icon-size" />
|
||||||
|
) : (
|
||||||
|
<ArrowDownIcon className="icon-size" />
|
||||||
|
)}
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,7 @@ import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { sortableElement, sortableHandle } from 'react-sortable-hoc';
|
import { sortableElement, sortableHandle } from 'react-sortable-hoc';
|
||||||
import { useInView } from 'react-intersection-observer';
|
import { useInView } from 'react-intersection-observer';
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
import { TrashIcon } from '@heroicons/react/outline';
|
||||||
|
|
||||||
import DescriptionInput from './DescriptionInput';
|
import DescriptionInput from './DescriptionInput';
|
||||||
import LibelleInput from './LibelleInput';
|
import LibelleInput from './LibelleInput';
|
||||||
|
@ -77,7 +77,8 @@ const TypeDeChamp = sortableElement(
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon icon="trash" title="Supprimer" />
|
<TrashIcon className="icon-size" />
|
||||||
|
<span className="screen-reader-text">Supprimer</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import React, { useReducer } from 'react';
|
import React, { useReducer } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
import { PlusIcon } from '@heroicons/react/outline';
|
||||||
|
|
||||||
import { SortableContainer, addChampLabel } from '../utils';
|
import { SortableContainer, addChampLabel } from '../utils';
|
||||||
import TypeDeChamp from './TypeDeChamp';
|
import TypeDeChamp from './TypeDeChamp';
|
||||||
|
@ -45,7 +45,7 @@ function TypeDeChampRepetitionOptions({
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon icon="plus" size="sm" />
|
<PlusIcon className="icon-size" />
|
||||||
|
|
||||||
{addChampLabel(state.isAnnotation)}
|
{addChampLabel(state.isAnnotation)}
|
||||||
</button>
|
</button>
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import React, { useReducer } from 'react';
|
import React, { useReducer } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
import { PlusIcon, ArrowCircleDownIcon } from '@heroicons/react/outline';
|
||||||
|
|
||||||
import { SortableContainer, addChampLabel } from '../utils';
|
import { SortableContainer, addChampLabel } from '../utils';
|
||||||
import TypeDeChamp from './TypeDeChamp';
|
import TypeDeChamp from './TypeDeChamp';
|
||||||
|
@ -38,7 +38,7 @@ function TypeDeChamps({ state: rootState, typeDeChamps }) {
|
||||||
</SortableContainer>
|
</SortableContainer>
|
||||||
{state.typeDeChamps.length === 0 && (
|
{state.typeDeChamps.length === 0 && (
|
||||||
<h2>
|
<h2>
|
||||||
<FontAwesomeIcon icon="arrow-circle-down" />
|
<ArrowCircleDownIcon className="icon-size" />
|
||||||
Cliquez sur le bouton «
|
Cliquez sur le bouton «
|
||||||
{addChampLabel(state.isAnnotation)} » pour créer votre premier
|
{addChampLabel(state.isAnnotation)} » pour créer votre premier
|
||||||
champ.
|
champ.
|
||||||
|
@ -56,7 +56,7 @@ function TypeDeChamps({ state: rootState, typeDeChamps }) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon icon="plus" size="sm" />
|
<PlusIcon className="icon-size" />
|
||||||
|
|
||||||
{addChampLabel(state.isAnnotation)}
|
{addChampLabel(state.isAnnotation)}
|
||||||
</button>
|
</button>
|
||||||
|
|
|
@ -1,27 +1,10 @@
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { library } from '@fortawesome/fontawesome-svg-core';
|
|
||||||
|
|
||||||
import { faArrowCircleDown } from '@fortawesome/free-solid-svg-icons/faArrowCircleDown';
|
|
||||||
import { faArrowDown } from '@fortawesome/free-solid-svg-icons/faArrowDown';
|
|
||||||
import { faArrowsAltV } from '@fortawesome/free-solid-svg-icons/faArrowsAltV';
|
|
||||||
import { faArrowUp } from '@fortawesome/free-solid-svg-icons/faArrowUp';
|
|
||||||
import { faPlus } from '@fortawesome/free-solid-svg-icons/faPlus';
|
|
||||||
import { faTrash } from '@fortawesome/free-solid-svg-icons/faTrash';
|
|
||||||
|
|
||||||
import Flash from './Flash';
|
import Flash from './Flash';
|
||||||
import OperationsQueue from './OperationsQueue';
|
import OperationsQueue from './OperationsQueue';
|
||||||
import TypeDeChamps from './components/TypeDeChamps';
|
import TypeDeChamps from './components/TypeDeChamps';
|
||||||
|
|
||||||
library.add(
|
|
||||||
faArrowCircleDown,
|
|
||||||
faArrowDown,
|
|
||||||
faArrowsAltV,
|
|
||||||
faArrowUp,
|
|
||||||
faPlus,
|
|
||||||
faTrash
|
|
||||||
);
|
|
||||||
|
|
||||||
class TypesDeChampEditor extends Component {
|
class TypesDeChampEditor extends Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
class ApplicationJob < ActiveJob::Base
|
class ApplicationJob < ActiveJob::Base
|
||||||
DEFAULT_MAX_ATTEMPTS_JOBS = 25
|
include ActiveJob::RetryOnTransientErrors
|
||||||
|
|
||||||
retry_on ::Excon::Error::BadRequest
|
DEFAULT_MAX_ATTEMPTS_JOBS = 25
|
||||||
|
|
||||||
before_perform do |job|
|
before_perform do |job|
|
||||||
Rails.logger.info("#{job.class.name} started at #{Time.zone.now}")
|
Rails.logger.info("#{job.class.name} started at #{Time.zone.now}")
|
||||||
|
|
|
@ -1,7 +0,0 @@
|
||||||
class ArchiveCreationJob < ApplicationJob
|
|
||||||
def perform(procedure, archive, instructeur)
|
|
||||||
ProcedureArchiveService
|
|
||||||
.new(procedure)
|
|
||||||
.collect_files_archive(archive, instructeur)
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -1,7 +0,0 @@
|
||||||
class Cron::PurgeStaleArchivesJob < Cron::CronJob
|
|
||||||
self.schedule_expression = "every 5 minutes"
|
|
||||||
|
|
||||||
def perform
|
|
||||||
Archive.stale.destroy_all
|
|
||||||
end
|
|
||||||
end
|
|
17
app/lib/active_job/retry_on_transient_errors.rb
Normal file
17
app/lib/active_job/retry_on_transient_errors.rb
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
module ActiveJob::RetryOnTransientErrors
|
||||||
|
extend ActiveSupport::Concern
|
||||||
|
|
||||||
|
TRANSIENT_ERRORS = [
|
||||||
|
Excon::Error::InternalServerError,
|
||||||
|
Excon::Error::GatewayTimeout,
|
||||||
|
Excon::Error::BadRequest
|
||||||
|
]
|
||||||
|
|
||||||
|
included do
|
||||||
|
if handler_for_rescue(TRANSIENT_ERRORS.first).nil?
|
||||||
|
TRANSIENT_ERRORS.each do |error_type|
|
||||||
|
retry_on error_type, attempts: 5, wait: :exponentially_longer
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -1,12 +1,14 @@
|
||||||
class ActiveStorage::DownloadableFile
|
class ActiveStorage::DownloadableFile
|
||||||
def self.create_list_from_dossier(dossier)
|
def self.create_list_from_dossier(dossier)
|
||||||
pjs = PiecesJustificativesService.liste_pieces_justificatives(dossier)
|
pjs = PiecesJustificativesService.liste_pieces_justificatives(dossier)
|
||||||
pjs.map do |piece_justificative|
|
files = pjs.map do |piece_justificative|
|
||||||
[
|
[
|
||||||
piece_justificative,
|
piece_justificative,
|
||||||
"dossier-#{dossier.id}/#{self.timestamped_filename(piece_justificative)}"
|
self.timestamped_filename(piece_justificative)
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
files << [dossier.pdf_export_for_instructeur, self.timestamped_filename(dossier.pdf_export_for_instructeur)]
|
||||||
|
files
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
@ -20,23 +22,19 @@ class ActiveStorage::DownloadableFile
|
||||||
timestamp = attachment.created_at.strftime("%d-%m-%Y-%H-%M")
|
timestamp = attachment.created_at.strftime("%d-%m-%Y-%H-%M")
|
||||||
id = attachment.id % 10000
|
id = attachment.id % 10000
|
||||||
|
|
||||||
[folder, "#{basename}-#{timestamp}-#{id}#{extension}"].join
|
"#{folder}/#{basename}-#{timestamp}-#{id}#{extension}"
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.folder(attachment)
|
def self.folder(attachment)
|
||||||
if attachment.name == 'pdf_export_for_instructeur'
|
|
||||||
return ''
|
|
||||||
end
|
|
||||||
|
|
||||||
case attachment.record_type
|
case attachment.record_type
|
||||||
when 'Dossier'
|
when 'Dossier'
|
||||||
'dossier/'
|
'dossier'
|
||||||
when 'DossierOperationLog', 'BillSignature'
|
when 'DossierOperationLog', 'BillSignature'
|
||||||
'horodatage/'
|
'horodatage'
|
||||||
when 'Commentaire'
|
when 'Commentaire'
|
||||||
'messagerie/'
|
'messagerie'
|
||||||
else
|
else
|
||||||
'pieces_justificatives/'
|
'pieces_justificatives'
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
# Preview all emails at http://localhost:3000/rails/mailers/instructeur_mailer
|
# Preview all emails at http://localhost:3000/rails/mailers/instructeur_mailer
|
||||||
class InstructeurMailer < ApplicationMailer
|
class InstructeurMailer < ApplicationMailer
|
||||||
helper MailerHelper
|
|
||||||
|
|
||||||
layout 'mailers/layout'
|
layout 'mailers/layout'
|
||||||
|
|
||||||
def user_to_instructeur(email)
|
def user_to_instructeur(email)
|
||||||
|
@ -44,12 +42,4 @@ class InstructeurMailer < ApplicationMailer
|
||||||
|
|
||||||
mail(to: instructeur.email, subject: subject)
|
mail(to: instructeur.email, subject: subject)
|
||||||
end
|
end
|
||||||
|
|
||||||
def send_archive(instructeur, procedure, archive)
|
|
||||||
@archive = archive
|
|
||||||
@procedure = procedure
|
|
||||||
subject = "Votre archive est disponible"
|
|
||||||
|
|
||||||
mail(to: instructeur.email, subject: subject)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,71 +0,0 @@
|
||||||
# == 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,12 +172,7 @@ class Dossier < ApplicationRecord
|
||||||
scope :en_construction, -> { not_archived.state_en_construction }
|
scope :en_construction, -> { not_archived.state_en_construction }
|
||||||
scope :en_instruction, -> { not_archived.state_en_instruction }
|
scope :en_instruction, -> { not_archived.state_en_instruction }
|
||||||
scope :termine, -> { not_archived.state_termine }
|
scope :termine, -> { not_archived.state_termine }
|
||||||
scope :processed_in_month, -> (month) do
|
scope :downloadable_sorted, -> {
|
||||||
state_termine
|
|
||||||
.joins(:traitements)
|
|
||||||
.where(traitements: { processed_at: month.beginning_of_month..month.end_of_month })
|
|
||||||
end
|
|
||||||
scope :downloadable_sorted, -> {
|
|
||||||
state_not_brouillon
|
state_not_brouillon
|
||||||
.includes(
|
.includes(
|
||||||
:user,
|
:user,
|
||||||
|
|
|
@ -25,7 +25,6 @@ class Instructeur < ApplicationRecord
|
||||||
has_many :followed_dossiers, through: :follows, source: :dossier
|
has_many :followed_dossiers, through: :follows, source: :dossier
|
||||||
has_many :previously_followed_dossiers, -> { distinct }, through: :previous_follows, source: :dossier
|
has_many :previously_followed_dossiers, -> { distinct }, through: :previous_follows, source: :dossier
|
||||||
has_many :trusted_device_tokens, dependent: :destroy
|
has_many :trusted_device_tokens, dependent: :destroy
|
||||||
has_many :archives
|
|
||||||
|
|
||||||
has_one :user, dependent: :nullify
|
has_one :user, dependent: :nullify
|
||||||
|
|
||||||
|
|
|
@ -156,20 +156,6 @@ class Procedure < ApplicationRecord
|
||||||
includes(:draft_revision, :published_revision, administrateurs: :user)
|
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 :libelle, presence: true, allow_blank: false, allow_nil: false
|
||||||
validates :description, presence: true, allow_blank: false, allow_nil: false
|
validates :description, presence: true, allow_blank: false, allow_nil: false
|
||||||
validates :administrateurs, presence: true
|
validates :administrateurs, presence: true
|
||||||
|
|
|
@ -1,11 +1,10 @@
|
||||||
class PiecesJustificativesService
|
class PiecesJustificativesService
|
||||||
def self.liste_pieces_justificatives(dossier)
|
def self.liste_pieces_justificatives(dossier)
|
||||||
dossier_export = generate_dossier_export(dossier)
|
|
||||||
pjs_champs = pjs_for_champs(dossier)
|
pjs_champs = pjs_for_champs(dossier)
|
||||||
pjs_commentaires = pjs_for_commentaires(dossier)
|
pjs_commentaires = pjs_for_commentaires(dossier)
|
||||||
pjs_dossier = pjs_for_dossier(dossier)
|
pjs_dossier = pjs_for_dossier(dossier)
|
||||||
|
|
||||||
([dossier_export] + pjs_champs + pjs_commentaires + pjs_dossier)
|
(pjs_champs + pjs_commentaires + pjs_dossier)
|
||||||
.filter(&:attached?)
|
.filter(&:attached?)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -44,17 +43,6 @@ class PiecesJustificativesService
|
||||||
|
|
||||||
private
|
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)
|
def self.pjs_for_champs(dossier)
|
||||||
allowed_champs = dossier.champs + dossier.champs_private
|
allowed_champs = dossier.champs + dossier.champs_private
|
||||||
|
|
||||||
|
|
|
@ -1,72 +0,0 @@
|
||||||
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
|
|
|
@ -1,21 +0,0 @@
|
||||||
- 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 +0,0 @@
|
||||||
= render_flash(sticky: true)
|
|
|
@ -1,91 +0,0 @@
|
||||||
- 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,6 +16,3 @@
|
||||||
- else
|
- else
|
||||||
%span{ 'data-export-poll-url': download_export_instructeur_procedure_path(procedure, export_format: format, no_progress_notification: true) }
|
%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])
|
= 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)
|
|
||||||
|
|
|
@ -14,6 +14,12 @@ ActiveSupport.on_load(:active_storage_attachment) do
|
||||||
include AttachmentVirusScannerConcern
|
include AttachmentVirusScannerConcern
|
||||||
end
|
end
|
||||||
|
|
||||||
|
Rails.application.reloader.to_prepare do
|
||||||
|
class ActiveStorage::BaseJob
|
||||||
|
include ActiveJob::RetryOnTransientErrors
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
# When an OpenStack service is initialized it makes a request to fetch
|
# When an OpenStack service is initialized it makes a request to fetch
|
||||||
# `publicURL` to use for all operations. We intercept the method that reads
|
# `publicURL` to use for all operations. We intercept the method that reads
|
||||||
# this url and replace the host with DS_Proxy host. This way all the operation
|
# this url and replace the host with DS_Proxy host. This way all the operation
|
||||||
|
|
|
@ -8,6 +8,3 @@ fr:
|
||||||
ods_html: Demander un export au format .ods
|
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_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})
|
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,8 +383,6 @@ Rails.application.routes.draw do
|
||||||
get 'telecharger_pjs' => 'dossiers#telecharger_pjs'
|
get 'telecharger_pjs' => 'dossiers#telecharger_pjs'
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
resources :archives, only: [:index, :create, :show], controller: 'archives'
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
get "recherche" => "recherche#index"
|
get "recherche" => "recherche#index"
|
||||||
|
|
|
@ -1,17 +0,0 @@
|
||||||
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
|
|
|
@ -1,5 +0,0 @@
|
||||||
class RenameContentTypeToToTimeSpanTypeForArchives < ActiveRecord::Migration[6.1]
|
|
||||||
def change
|
|
||||||
rename_column :archives, :content_type, :time_span_type
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -1,6 +0,0 @@
|
||||||
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.
|
# It's strongly recommended that you check this file into your version control system.
|
||||||
|
|
||||||
ActiveRecord::Schema.define(version: 2021_04_27_124500) do
|
ActiveRecord::Schema.define(version: 2021_04_27_120002) do
|
||||||
|
|
||||||
# These are extensions that must be enabled in order to support this database
|
# These are extensions that must be enabled in order to support this database
|
||||||
enable_extension "plpgsql"
|
enable_extension "plpgsql"
|
||||||
|
@ -81,25 +81,6 @@ ActiveRecord::Schema.define(version: 2021_04_27_124500) do
|
||||||
t.index ["procedure_id"], name: "index_administrateurs_procedures_on_procedure_id"
|
t.index ["procedure_id"], name: "index_administrateurs_procedures_on_procedure_id"
|
||||||
end
|
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|
|
create_table "assign_tos", id: :serial, force: :cascade do |t|
|
||||||
t.integer "instructeur_id"
|
t.integer "instructeur_id"
|
||||||
t.datetime "created_at"
|
t.datetime "created_at"
|
||||||
|
@ -754,8 +735,6 @@ ActiveRecord::Schema.define(version: 2021_04_27_124500) do
|
||||||
end
|
end
|
||||||
|
|
||||||
add_foreign_key "active_storage_variant_records", "active_storage_blobs", column: "blob_id"
|
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 "assign_tos", "groupe_instructeurs"
|
||||||
add_foreign_key "attestation_templates", "procedures"
|
add_foreign_key "attestation_templates", "procedures"
|
||||||
add_foreign_key "attestations", "dossiers"
|
add_foreign_key "attestations", "dossiers"
|
||||||
|
|
|
@ -1,9 +1,6 @@
|
||||||
{
|
{
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/preset-react": "^7.12.13",
|
"@babel/preset-react": "^7.12.13",
|
||||||
"@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",
|
"@heroicons/react": "^1.0.1",
|
||||||
"@mapbox/mapbox-gl-draw": "^1.2.2",
|
"@mapbox/mapbox-gl-draw": "^1.2.2",
|
||||||
"@rails/actiontext": "^6.0.3",
|
"@rails/actiontext": "^6.0.3",
|
||||||
|
|
|
@ -1,56 +0,0 @@
|
||||||
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,6 +712,13 @@ describe Instructeurs::DossiersController, type: :controller do
|
||||||
dossier_id: dossier.id
|
dossier_id: dossier.id
|
||||||
}
|
}
|
||||||
end
|
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
|
end
|
||||||
|
|
||||||
describe "#delete_dossier" do
|
describe "#delete_dossier" do
|
||||||
|
|
|
@ -1,15 +0,0 @@
|
||||||
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,8 +12,6 @@ feature 'Inviting an expert:', js: true do
|
||||||
|
|
||||||
context 'as an Instructeur' do
|
context 'as an Instructeur' do
|
||||||
scenario 'I can invite an expert' do
|
scenario 'I can invite an expert' do
|
||||||
allow(ClamavService).to receive(:safe_file?).and_return(true)
|
|
||||||
|
|
||||||
# assign instructeur to linked dossier
|
# assign instructeur to linked dossier
|
||||||
instructeur.assign_to_procedure(linked_dossier.procedure)
|
instructeur.assign_to_procedure(linked_dossier.procedure)
|
||||||
|
|
||||||
|
|
|
@ -165,10 +165,10 @@ feature 'Instructing a dossier:', js: true do
|
||||||
|
|
||||||
expect(DownloadHelpers.download).to include "dossier-#{dossier.id}.zip"
|
expect(DownloadHelpers.download).to include "dossier-#{dossier.id}.zip"
|
||||||
expect(files.size).to be 3
|
expect(files.size).to be 3
|
||||||
expect(files[0].filename.include?('export')).to be_truthy
|
expect(files[0].filename.include?('piece_justificative_0')).to be_truthy
|
||||||
expect(files[1].filename.include?('piece_justificative_0')).to be_truthy
|
expect(files[0].uncompressed_size).to be File.size(path)
|
||||||
expect(files[1].uncompressed_size).to be File.size(path)
|
expect(files[1].filename.include?('horodatage/operation')).to be_truthy
|
||||||
expect(files[2].filename.include?('horodatage/operation')).to be_truthy
|
expect(files[2].filename.include?('dossier/export')).to be_truthy
|
||||||
end
|
end
|
||||||
|
|
||||||
scenario 'A instructeur can download an archive containing several identical attachments' do
|
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(DownloadHelpers.download).to include "dossier-#{dossier.id}.zip"
|
||||||
expect(files.size).to be 4
|
expect(files.size).to be 4
|
||||||
expect(files[0].filename.include?('export')).to be_truthy
|
expect(files[0].filename.include?('piece_justificative_0')).to be_truthy
|
||||||
expect(files[1].filename.include?('piece_justificative_0')).to be_truthy
|
expect(files[1].filename.include?('piece_justificative_0')).to be_truthy
|
||||||
expect(files[2].filename.include?('piece_justificative_0')).to be_truthy
|
expect(files[0].filename).not_to eq files[1].filename
|
||||||
expect(files[1].filename).not_to eq files[2].filename
|
expect(files[0].uncompressed_size).to be File.size(path)
|
||||||
expect(files[1].uncompressed_size).to be File.size(path)
|
expect(files[1].uncompressed_size).to be File.size(path)
|
||||||
expect(files[2].uncompressed_size).to be File.size(path)
|
expect(files[2].filename.include?('horodatage/operation')).to be_truthy
|
||||||
expect(files[3].filename.include?('horodatage/operation')).to be_truthy
|
expect(files[3].filename.include?('dossier/export')).to be_truthy
|
||||||
end
|
end
|
||||||
|
|
||||||
before { DownloadHelpers.clear_downloads }
|
before { DownloadHelpers.clear_downloads }
|
||||||
|
|
3
spec/jobs/active_storage/base_job_spec.rb
Normal file
3
spec/jobs/active_storage/base_job_spec.rb
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
describe ActiveStorage::BaseJob do
|
||||||
|
it_behaves_like 'a job retrying transient errors'
|
||||||
|
end
|
|
@ -1,6 +1,8 @@
|
||||||
include ActiveJob::TestHelper
|
include ActiveJob::TestHelper
|
||||||
|
|
||||||
RSpec.describe ApplicationJob, type: :job do
|
RSpec.describe ApplicationJob, type: :job do
|
||||||
|
it_behaves_like 'a job retrying transient errors'
|
||||||
|
|
||||||
describe 'perform' do
|
describe 'perform' do
|
||||||
before do
|
before do
|
||||||
allow(Rails.logger).to receive(:info)
|
allow(Rails.logger).to receive(:info)
|
||||||
|
@ -13,23 +15,7 @@ RSpec.describe ApplicationJob, type: :job do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when ::Excon::Error::BadRequest is raised' do
|
|
||||||
# https://api.rubyonrails.org/classes/ActiveJob/Exceptions/ClassMethods.html#method-i-retry_on
|
|
||||||
# retry on will try 5 times and then bubble up the error
|
|
||||||
it 'makes 5 attempts' do
|
|
||||||
assert_performed_jobs 5 do
|
|
||||||
ExconErrJob.perform_later rescue ::Excon::Error::BadRequest
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
class ChildJob < ApplicationJob
|
class ChildJob < ApplicationJob
|
||||||
def perform; end
|
def perform; end
|
||||||
end
|
end
|
||||||
|
|
||||||
class ExconErrJob < ApplicationJob
|
|
||||||
def perform
|
|
||||||
raise ::Excon::Error::BadRequest.new('bad request')
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
9
spec/lib/active_job/retry_on_transient_errors_spec.rb
Normal file
9
spec/lib/active_job/retry_on_transient_errors_spec.rb
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
describe ActiveJob::RetryOnTransientErrors do
|
||||||
|
# rubocop:disable Rails/ApplicationJob
|
||||||
|
class Job < ActiveJob::Base
|
||||||
|
include ActiveJob::RetryOnTransientErrors
|
||||||
|
end
|
||||||
|
# rubocop:enable Rails/ApplicationJob
|
||||||
|
|
||||||
|
it_behaves_like 'a job retrying transient errors', Job
|
||||||
|
end
|
|
@ -1,51 +0,0 @@
|
||||||
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,12 +16,7 @@ describe PiecesJustificativesService do
|
||||||
# to be exported
|
# to be exported
|
||||||
it 'ensures no titre identite is given' do
|
it 'ensures no titre identite is given' do
|
||||||
expect(champ_identite.piece_justificative_file).to be_attached
|
expect(champ_identite.piece_justificative_file).to be_attached
|
||||||
expect(subject.any? { |piece| piece.name == 'piece_justificative_file' }).to be_falsy
|
expect(subject).to eq([])
|
||||||
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
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,83 +0,0 @@
|
||||||
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
|
|
29
spec/support/shared_examples_for_jobs.rb
Normal file
29
spec/support/shared_examples_for_jobs.rb
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
RSpec.shared_examples 'a job retrying transient errors' do |job_class = described_class|
|
||||||
|
context 'when a transient network error is raised' do
|
||||||
|
ExconErrorJob = Class.new(job_class) do
|
||||||
|
def perform
|
||||||
|
raise Excon::Error::InternalServerError, 'msg'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'makes 5 attempts before raising the exception up' do
|
||||||
|
assert_performed_jobs 5 do
|
||||||
|
ExconErrorJob.perform_later rescue Excon::Error::InternalServerError
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when another type of error is raised' do
|
||||||
|
StandardErrorJob = Class.new(job_class) do
|
||||||
|
def perform
|
||||||
|
raise StandardError
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'makes only 1 attempt before raising the exception up' do
|
||||||
|
assert_performed_jobs 1 do
|
||||||
|
StandardErrorJob.perform_later rescue StandardError
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -12,15 +12,5 @@ describe 'instructeurs/procedures/_download_dossiers.html.haml', type: :view do
|
||||||
context "when procedure has at least 1 dossier" do
|
context "when procedure has at least 1 dossier" do
|
||||||
let(:dossier_count) { 1 }
|
let(:dossier_count) { 1 }
|
||||||
it { is_expected.to include("Télécharger tous les dossiers") }
|
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
|
||||||
end
|
end
|
||||||
|
|
26
yarn.lock
26
yarn.lock
|
@ -1005,32 +1005,6 @@
|
||||||
enabled "2.0.x"
|
enabled "2.0.x"
|
||||||
kuler "^2.0.0"
|
kuler "^2.0.0"
|
||||||
|
|
||||||
"@fortawesome/fontawesome-common-types@^0.2.34":
|
|
||||||
version "0.2.34"
|
|
||||||
resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-0.2.34.tgz#0a8c348bb23b7b760030f5b1d912e582be4ec915"
|
|
||||||
integrity sha512-XcIn3iYbTEzGIxD0/dY5+4f019jIcEIWBiHc3KrmK/ROahwxmZ/s+tdj97p/5K0klz4zZUiMfUlYP0ajhSJjmA==
|
|
||||||
|
|
||||||
"@fortawesome/fontawesome-svg-core@^1.2.34":
|
|
||||||
version "1.2.34"
|
|
||||||
resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-1.2.34.tgz#1d1a7c92537cbc2b8a83eef6b6d824b4b5b46b26"
|
|
||||||
integrity sha512-0KNN0nc5eIzaJxlv43QcDmTkDY1CqeN6J7OCGSs+fwGPdtv0yOQqRjieopBCmw+yd7uD3N2HeNL3Zm5isDleLg==
|
|
||||||
dependencies:
|
|
||||||
"@fortawesome/fontawesome-common-types" "^0.2.34"
|
|
||||||
|
|
||||||
"@fortawesome/free-solid-svg-icons@^5.15.2":
|
|
||||||
version "5.15.2"
|
|
||||||
resolved "https://registry.yarnpkg.com/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-5.15.2.tgz#25bb035de57cf85aee8072965732368ccc8e8943"
|
|
||||||
integrity sha512-ZfCU+QjaFsdNZmOGmfqEWhzI3JOe37x5dF4kz9GeXvKn/sTxhqMtZ7mh3lBf76SvcYY5/GKFuyG7p1r4iWMQqw==
|
|
||||||
dependencies:
|
|
||||||
"@fortawesome/fontawesome-common-types" "^0.2.34"
|
|
||||||
|
|
||||||
"@fortawesome/react-fontawesome@^0.1.14":
|
|
||||||
version "0.1.14"
|
|
||||||
resolved "https://registry.yarnpkg.com/@fortawesome/react-fontawesome/-/react-fontawesome-0.1.14.tgz#bf28875c3935b69ce2dc620e1060b217a47f64ca"
|
|
||||||
integrity sha512-4wqNb0gRLVaBm/h+lGe8UfPPivcbuJ6ecI4hIgW0LjI7kzpYB9FkN0L9apbVzg+lsBdcTf0AlBtODjcSX5mmKA==
|
|
||||||
dependencies:
|
|
||||||
prop-types "^15.7.2"
|
|
||||||
|
|
||||||
"@heroicons/react@^1.0.1":
|
"@heroicons/react@^1.0.1":
|
||||||
version "1.0.1"
|
version "1.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/@heroicons/react/-/react-1.0.1.tgz#66d25f6441920bd5c2146ea27fd33995885452dd"
|
resolved "https://registry.yarnpkg.com/@heroicons/react/-/react-1.0.1.tgz#66d25f6441920bd5c2146ea27fd33995885452dd"
|
||||||
|
|
Loading…
Reference in a new issue