commit
1348587356
25 changed files with 485 additions and 10 deletions
1
app/assets/images/icons/retry.svg
Normal file
1
app/assets/images/icons/retry.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24"><switch transform="translate(0 -101)"><g fill="#0069cc"><path d="M21.406 118.453a1.422 1.422 0 0 0-.56-1.888c-.69-.372-1.298.088-1.67.778-.836 1.592-2.266 2.919-4.049 3.76-3.765 1.769-8.693.51-11.063-2.9-2.87-4.143-1.409-10.218 2.871-12.7 3.98-2.302 9.506-.862 11.925 3.05l-1.96 1.162c-.508.302-.502 1.049.036 1.323l5.798 2.966a.755.755 0 0 0 1.11-.659l.156-6.473c.016-.604-.637-.967-1.146-.664l-1.78 1.057c-3.384-5.589-10.81-7.243-16.272-3.434-4.541 3.2-6.107 9.354-3.633 14.358 2.977 5.956 10.364 8.071 16.004 4.72a11.39 11.39 0 0 0 4.233-4.456z"/></g></switch></svg>
|
After Width: | Height: | Size: 632 B |
|
@ -10,6 +10,7 @@ $dark-red: #A10005;
|
||||||
$medium-red: rgba(161, 0, 5, 0.9);
|
$medium-red: rgba(161, 0, 5, 0.9);
|
||||||
$light-red: #ED1C24;
|
$light-red: #ED1C24;
|
||||||
$lighter-red: #F52A2A;
|
$lighter-red: #F52A2A;
|
||||||
|
$background-red: #FFDFDF;
|
||||||
$green: #15AD70;
|
$green: #15AD70;
|
||||||
$lighter-green: lighten($green, 30%);
|
$lighter-green: lighten($green, 30%);
|
||||||
$light-green: lighten($green, 25%);
|
$light-green: lighten($green, 25%);
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
@import "colors";
|
||||||
@import "constants";
|
@import "constants";
|
||||||
|
|
||||||
.attachment-actions {
|
.attachment-actions {
|
||||||
|
@ -13,6 +14,38 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.attachment-error {
|
||||||
|
display: flex;
|
||||||
|
width: max-content;
|
||||||
|
max-width: 100%;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: $default-padding;
|
||||||
|
padding: $default-padding;
|
||||||
|
background: $background-red;
|
||||||
|
|
||||||
|
&.hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.attachment-error-message {
|
||||||
|
display: inline-block;
|
||||||
|
margin-right: $default-padding;
|
||||||
|
color: $medium-red;
|
||||||
|
}
|
||||||
|
|
||||||
|
.attachment-error-title {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.attachment-error-retry {
|
||||||
|
white-space: nowrap;
|
||||||
|
|
||||||
|
&.hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.attachment-input.hidden {
|
.attachment-input.hidden {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,6 +33,7 @@
|
||||||
border-color: red;
|
border-color: red;
|
||||||
}
|
}
|
||||||
|
|
||||||
input[type=file][data-direct-upload-url][disabled] {
|
input[type=file][data-direct-upload-url][disabled],
|
||||||
|
input[type=file][data-auto-attach-url][disabled] {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
|
@ -67,6 +67,10 @@
|
||||||
background-image: image-url("icons/preview.svg");
|
background-image: image-url("icons/preview.svg");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.retry {
|
||||||
|
background-image: image-url("icons/retry.svg");
|
||||||
|
}
|
||||||
|
|
||||||
&.download {
|
&.download {
|
||||||
background-image: image-url("icons/download.svg");
|
background-image: image-url("icons/download.svg");
|
||||||
}
|
}
|
||||||
|
|
21
app/controllers/champs/piece_justificative_controller.rb
Normal file
21
app/controllers/champs/piece_justificative_controller.rb
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
class Champs::PieceJustificativeController < ApplicationController
|
||||||
|
before_action :authenticate_logged_user!
|
||||||
|
|
||||||
|
def update
|
||||||
|
@champ = policy_scope(Champ).find(params[:champ_id])
|
||||||
|
|
||||||
|
@champ.piece_justificative_file.attach(params[:blob_signed_id])
|
||||||
|
if @champ.save
|
||||||
|
render :show
|
||||||
|
else
|
||||||
|
errors = @champ.errors.full_messages
|
||||||
|
|
||||||
|
# Before Rails 6, the attachment was persisted to database
|
||||||
|
# by 'attach', even before calling save.
|
||||||
|
# So until we're on Rails 6, we need to purge the file explicitely.
|
||||||
|
@champ.piece_justificative_file.purge_later
|
||||||
|
|
||||||
|
render :json => { errors: errors }, :status => 422
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -31,4 +31,10 @@ module ChampHelper
|
||||||
"desc-#{champ.type_de_champ.id}-#{champ.row}"
|
"desc-#{champ.type_de_champ.id}-#{champ.row}"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def auto_attach_url(form, object)
|
||||||
|
if feature_enabled?(:autoupload_dossier_attachments) && object.is_a?(Champ) && object.public?
|
||||||
|
champs_piece_justificative_url(form.index)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
141
app/javascript/new_design/dossiers/auto-upload-controller.js
Normal file
141
app/javascript/new_design/dossiers/auto-upload-controller.js
Normal file
|
@ -0,0 +1,141 @@
|
||||||
|
import Uploader from '../../shared/activestorage/uploader';
|
||||||
|
import ProgressBar from '../../shared/activestorage/progress-bar';
|
||||||
|
import { ajax, show, hide, toggle } from '@utils';
|
||||||
|
|
||||||
|
// Given a file input in a champ with a selected file, upload a file,
|
||||||
|
// then attach it to the dossier.
|
||||||
|
//
|
||||||
|
// On success, the champ is replaced by an HTML fragment describing the attachment.
|
||||||
|
// On error, a error message is displayed above the input.
|
||||||
|
export default class AutoUploadController {
|
||||||
|
constructor(input, file) {
|
||||||
|
this.input = input;
|
||||||
|
this.file = file;
|
||||||
|
}
|
||||||
|
|
||||||
|
async start() {
|
||||||
|
try {
|
||||||
|
this._begin();
|
||||||
|
|
||||||
|
// Sanity checks
|
||||||
|
const autoAttachUrl = this.input.dataset.autoAttachUrl;
|
||||||
|
if (!autoAttachUrl) {
|
||||||
|
throw new Error('L’attribut "data-auto-attach-url" est manquant');
|
||||||
|
}
|
||||||
|
|
||||||
|
const champ = this.input.closest('.editable-champ[data-champ-id]');
|
||||||
|
if (!champ) {
|
||||||
|
throw new Error('Impossible de trouver l’élément ".editable-champ"');
|
||||||
|
}
|
||||||
|
const champId = champ.dataset.champId;
|
||||||
|
|
||||||
|
// Upload the file (using Direct Upload)
|
||||||
|
let blobSignedId = await this._upload();
|
||||||
|
|
||||||
|
// Attach the blob to the champ
|
||||||
|
// (The request responds with Javascript, which displays the attachment HTML fragment).
|
||||||
|
await this._attach(champId, blobSignedId, autoAttachUrl);
|
||||||
|
|
||||||
|
// Everything good: clear the original file input value
|
||||||
|
this.input.value = null;
|
||||||
|
} catch (error) {
|
||||||
|
this._failed(error);
|
||||||
|
throw error;
|
||||||
|
} finally {
|
||||||
|
this._done();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_begin() {
|
||||||
|
this.input.disabled = true;
|
||||||
|
this._hideErrorMessage();
|
||||||
|
}
|
||||||
|
|
||||||
|
async _upload() {
|
||||||
|
const uploader = new Uploader(
|
||||||
|
this.input,
|
||||||
|
this.file,
|
||||||
|
this.input.dataset.directUploadUrl
|
||||||
|
);
|
||||||
|
return await uploader.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
async _attach(champId, blobSignedId, autoAttachUrl) {
|
||||||
|
// Now that the upload is done, display a new progress bar
|
||||||
|
// to show that the attachment request is still pending.
|
||||||
|
const progressBar = new ProgressBar(this.input, champId, this.file);
|
||||||
|
progressBar.progress(100);
|
||||||
|
progressBar.end();
|
||||||
|
|
||||||
|
const attachmentRequest = {
|
||||||
|
url: autoAttachUrl,
|
||||||
|
type: 'PUT',
|
||||||
|
data: `champ_id=${champId}&blob_signed_id=${blobSignedId}`
|
||||||
|
};
|
||||||
|
await ajax(attachmentRequest);
|
||||||
|
|
||||||
|
// The progress bar has been destroyed by the attachment HTML fragment that replaced the input,
|
||||||
|
// so no further cleanup is needed.
|
||||||
|
}
|
||||||
|
|
||||||
|
_failed(error) {
|
||||||
|
if (!document.body.contains(this.input)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let progressBar = this.input.parentElement.querySelector('.direct-upload');
|
||||||
|
if (progressBar) {
|
||||||
|
progressBar.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
this._displayErrorMessage(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
_done() {
|
||||||
|
this.input.disabled = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
_messageFromError(error) {
|
||||||
|
if (
|
||||||
|
error.xhr &&
|
||||||
|
error.xhr.status == 422 &&
|
||||||
|
error.response &&
|
||||||
|
error.response.errors &&
|
||||||
|
error.response.errors[0]
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
title: error.response.errors[0],
|
||||||
|
description: '',
|
||||||
|
retry: false
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
title: 'Une erreur s’est produite pendant l’envoi du fichier.',
|
||||||
|
description: error.message || error.toString(),
|
||||||
|
retry: true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_displayErrorMessage(error) {
|
||||||
|
let errorNode = this.input.parentElement.querySelector('.attachment-error');
|
||||||
|
if (errorNode) {
|
||||||
|
show(errorNode);
|
||||||
|
let message = this._messageFromError(error);
|
||||||
|
errorNode.querySelector('.attachment-error-title').textContent =
|
||||||
|
message.title || '';
|
||||||
|
errorNode.querySelector('.attachment-error-description').textContent =
|
||||||
|
message.description || '';
|
||||||
|
toggle(errorNode.querySelector('.attachment-error-retry'), message.retry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_hideErrorMessage() {
|
||||||
|
let errorElement = this.input.parentElement.querySelector(
|
||||||
|
'.attachment-error'
|
||||||
|
);
|
||||||
|
if (errorElement) {
|
||||||
|
hide(errorElement);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
23
app/javascript/new_design/dossiers/auto-upload.js
Normal file
23
app/javascript/new_design/dossiers/auto-upload.js
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
import AutoUploadsControllers from './auto-uploads-controllers.js';
|
||||||
|
import { delegate } from '@utils';
|
||||||
|
|
||||||
|
// Create a controller responsible for managing several concurrent uploads.
|
||||||
|
const autoUploadsControllers = new AutoUploadsControllers();
|
||||||
|
|
||||||
|
function startUpload(input) {
|
||||||
|
Array.from(input.files).forEach(file => {
|
||||||
|
autoUploadsControllers.upload(input, file);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const fileInputSelector = `input[type=file][data-direct-upload-url][data-auto-attach-url]:not([disabled])`;
|
||||||
|
delegate('change', fileInputSelector, event => {
|
||||||
|
startUpload(event.target);
|
||||||
|
});
|
||||||
|
|
||||||
|
const retryButtonSelector = `button.attachment-error-retry`;
|
||||||
|
delegate('click', retryButtonSelector, event => {
|
||||||
|
const inputSelector = event.target.dataset.inputTarget;
|
||||||
|
const input = document.querySelector(inputSelector);
|
||||||
|
startUpload(input);
|
||||||
|
});
|
|
@ -0,0 +1,46 @@
|
||||||
|
import Rails from '@rails/ujs';
|
||||||
|
import AutoUploadController from './auto-upload-controller.js';
|
||||||
|
|
||||||
|
// Manage multiple concurrent uploads.
|
||||||
|
//
|
||||||
|
// When the first upload starts, all the form "Submit" buttons are disabled.
|
||||||
|
// They are enabled again when the last upload ends.
|
||||||
|
export default class AutoUploadsControllers {
|
||||||
|
constructor() {
|
||||||
|
this.inFlightUploadsCount = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
async upload(input, file) {
|
||||||
|
let form = input.form;
|
||||||
|
this._incrementInFlightUploads(form);
|
||||||
|
|
||||||
|
try {
|
||||||
|
let controller = new AutoUploadController(input, file);
|
||||||
|
await controller.start();
|
||||||
|
} finally {
|
||||||
|
this._decrementInFlightUploads(form);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_incrementInFlightUploads(form) {
|
||||||
|
this.inFlightUploadsCount += 1;
|
||||||
|
|
||||||
|
if (form) {
|
||||||
|
form
|
||||||
|
.querySelectorAll('button[type=submit]')
|
||||||
|
.forEach(submitButton => Rails.disableElement(submitButton));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_decrementInFlightUploads(form) {
|
||||||
|
if (this.inFlightUploadsCount > 0) {
|
||||||
|
this.inFlightUploadsCount -= 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.inFlightUploadsCount == 0 && form) {
|
||||||
|
form
|
||||||
|
.querySelectorAll('button[type=submit]')
|
||||||
|
.forEach(submitButton => Rails.enableElement(submitButton));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -23,6 +23,7 @@ import '../new_design/select2';
|
||||||
import '../new_design/spinner';
|
import '../new_design/spinner';
|
||||||
import '../new_design/support';
|
import '../new_design/support';
|
||||||
import '../new_design/dossiers/auto-save';
|
import '../new_design/dossiers/auto-save';
|
||||||
|
import '../new_design/dossiers/auto-upload';
|
||||||
|
|
||||||
import '../new_design/champs/carte';
|
import '../new_design/champs/carte';
|
||||||
import '../new_design/champs/linked-drop-down-list';
|
import '../new_design/champs/linked-drop-down-list';
|
||||||
|
|
|
@ -41,7 +41,7 @@ export default class ProgressBar {
|
||||||
}
|
}
|
||||||
|
|
||||||
static render(id, filename) {
|
static render(id, filename) {
|
||||||
return `<div id="direct-upload-${id}" class="direct-upload ${PENDING_CLASS}">
|
return `<div id="direct-upload-${id}" class="direct-upload ${PENDING_CLASS}" data-direct-upload-id="${id}">
|
||||||
<div class="direct-upload__progress" style="width: 0%"></div>
|
<div class="direct-upload__progress" style="width: 0%"></div>
|
||||||
<span class="direct-upload__filename">${filename}</span>
|
<span class="direct-upload__filename">${filename}</span>
|
||||||
</div>`;
|
</div>`;
|
||||||
|
|
|
@ -27,6 +27,8 @@ addUploadEventListener(INITIALIZE_EVENT, ({ target, detail: { id, file } }) => {
|
||||||
|
|
||||||
addUploadEventListener(START_EVENT, ({ target, detail: { id } }) => {
|
addUploadEventListener(START_EVENT, ({ target, detail: { id } }) => {
|
||||||
ProgressBar.start(id);
|
ProgressBar.start(id);
|
||||||
|
// At the end of the upload, the form will be submitted again.
|
||||||
|
// Avoid the confirm dialog to be presented again then.
|
||||||
const button = target.form.querySelector('button.primary');
|
const button = target.form.querySelector('button.primary');
|
||||||
if (button) {
|
if (button) {
|
||||||
button.removeAttribute('data-confirm');
|
button.removeAttribute('data-confirm');
|
||||||
|
|
|
@ -3,7 +3,7 @@ import ProgressBar from './progress-bar';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Uploader class is a delegate for DirectUpload instance
|
Uploader class is a delegate for DirectUpload instance
|
||||||
used to track lifecycle and progress of un upload.
|
used to track lifecycle and progress of an upload.
|
||||||
*/
|
*/
|
||||||
export default class Uploader {
|
export default class Uploader {
|
||||||
constructor(input, file, directUploadUrl) {
|
constructor(input, file, directUploadUrl) {
|
||||||
|
|
|
@ -29,6 +29,13 @@ delegate('click', '[data-attachment-refresh]', event => {
|
||||||
attachementPoller.check();
|
attachementPoller.check();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Periodically check the state of a set of URLs.
|
||||||
|
//
|
||||||
|
// Each time the given URL is requested, the matching `show.js.erb` view is rendered,
|
||||||
|
// causing the state to be refreshed.
|
||||||
|
//
|
||||||
|
// This is used mainly to refresh attachments during the anti-virus check,
|
||||||
|
// but also to refresh the state of a pending spreadsheet export.
|
||||||
class RemotePoller {
|
class RemotePoller {
|
||||||
urls = new Set();
|
urls = new Set();
|
||||||
timeout;
|
timeout;
|
||||||
|
|
|
@ -13,8 +13,14 @@ export function hide(el) {
|
||||||
el && el.classList.add('hidden');
|
el && el.classList.add('hidden');
|
||||||
}
|
}
|
||||||
|
|
||||||
export function toggle(el) {
|
export function toggle(el, force) {
|
||||||
el && el.classList.toggle('hidden');
|
if (force == undefined) {
|
||||||
|
el & el.classList.toggle('hidden');
|
||||||
|
} else if (force) {
|
||||||
|
el && el.classList.remove('hidden');
|
||||||
|
} else {
|
||||||
|
el && el.classList.add('hidden');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function enable(el) {
|
export function enable(el) {
|
||||||
|
|
17
app/views/champs/piece_justificative/show.js.erb
Normal file
17
app/views/champs/piece_justificative/show.js.erb
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
<% dossier = @champ.dossier %>
|
||||||
|
|
||||||
|
<%= fields_for dossier do |form| %>
|
||||||
|
<%= form.fields_for :champs, dossier.champs.where(id: @champ.id), include_id: false do |champ_form| %>
|
||||||
|
<% render_to_element(".editable-champ[data-champ-id=\"#{@champ.id}\"]",
|
||||||
|
partial: 'shared/dossiers/editable_champs/editable_champ',
|
||||||
|
locals: {
|
||||||
|
champ: @champ,
|
||||||
|
form: champ_form
|
||||||
|
}) %>
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<% attachment = @champ.piece_justificative_file.attachment %>
|
||||||
|
<% if attachment.virus_scanner.pending? %>
|
||||||
|
<%= fire_event('attachment:update', { url: attachment_url(attachment.id, { signed_id: attachment.blob.signed_id, user_can_upload: true }) }.to_json ) %>
|
||||||
|
<% end %>
|
|
@ -32,6 +32,7 @@
|
||||||
%span.icon.phone
|
%span.icon.phone
|
||||||
%span.icon.clock
|
%span.icon.clock
|
||||||
%span.icon.preview
|
%span.icon.preview
|
||||||
|
%span.icon.retry
|
||||||
%span.icon.download
|
%span.icon.download
|
||||||
%span.icon.download-white
|
%span.icon.download-white
|
||||||
%span.icon.move-handle
|
%span.icon.move-handle
|
||||||
|
|
|
@ -22,7 +22,17 @@
|
||||||
.attachment-action
|
.attachment-action
|
||||||
= button_tag 'Remplacer', type: 'button', class: 'button small', data: { 'toggle-target': ".attachment-input-#{attachment_id}" }
|
= button_tag 'Remplacer', type: 'button', class: 'button small', data: { 'toggle-target': ".attachment-input-#{attachment_id}" }
|
||||||
|
|
||||||
|
.attachment-error.hidden
|
||||||
|
.attachment-error-message
|
||||||
|
%p.attachment-error-title
|
||||||
|
Une erreur s’est produite pendant l’envoi du fichier.
|
||||||
|
%p.attachment-error-description
|
||||||
|
= button_tag type: 'button', class: 'button attachment-error-retry', data: { 'input-target': ".attachment-input-#{attachment_id}" } do
|
||||||
|
%span.icon.retry
|
||||||
|
Ré-essayer
|
||||||
|
|
||||||
= form.file_field attached_file.name,
|
= form.file_field attached_file.name,
|
||||||
class: "attachment-input attachment-input-#{attachment_id} #{'hidden' if persisted}",
|
class: "attachment-input attachment-input-#{attachment_id} #{'hidden' if persisted}",
|
||||||
accept: accept,
|
accept: accept,
|
||||||
direct_upload: true
|
direct_upload: true,
|
||||||
|
data: { 'auto-attach-url': auto_attach_url(form, form.object) }
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
.editable-champ{ class: "editable-champ-#{champ.type_champ}" }
|
.editable-champ{ class: "editable-champ-#{champ.type_champ}", data: { 'champ-id': champ.id } }
|
||||||
- if champ.repetition?
|
- if champ.repetition?
|
||||||
%h3.header-subsection= champ.libelle
|
%h3.header-subsection= champ.libelle
|
||||||
- if champ.description.present?
|
- if champ.description.present?
|
||||||
|
|
|
@ -31,6 +31,7 @@ features = [
|
||||||
:insee_api_v3,
|
:insee_api_v3,
|
||||||
:instructeur_bypass_email_login_token,
|
:instructeur_bypass_email_login_token,
|
||||||
:autosave_dossier_draft,
|
:autosave_dossier_draft,
|
||||||
|
:autoupload_dossier_attachments,
|
||||||
:maintenance_mode,
|
:maintenance_mode,
|
||||||
:mini_profiler,
|
:mini_profiler,
|
||||||
:operation_log_serialize_subject,
|
:operation_log_serialize_subject,
|
||||||
|
|
|
@ -119,6 +119,7 @@ Rails.application.routes.draw do
|
||||||
get ':position/dossier_link', to: 'dossier_link#show', as: :dossier_link
|
get ':position/dossier_link', to: 'dossier_link#show', as: :dossier_link
|
||||||
post ':position/carte', to: 'carte#show', as: :carte
|
post ':position/carte', to: 'carte#show', as: :carte
|
||||||
post ':position/repetition', to: 'repetition#show', as: :repetition
|
post ':position/repetition', to: 'repetition#show', as: :repetition
|
||||||
|
put ':position/piece_justificative', to: 'piece_justificative#update', as: :piece_justificative
|
||||||
end
|
end
|
||||||
|
|
||||||
get 'attachments/:id', to: 'attachments#show', as: :attachment
|
get 'attachments/:id', to: 'attachments#show', as: :attachment
|
||||||
|
|
|
@ -0,0 +1,65 @@
|
||||||
|
describe Champs::PieceJustificativeController, type: :controller do
|
||||||
|
let(:user) { create(:user) }
|
||||||
|
let(:procedure) { create(:procedure, :published, :with_piece_justificative) }
|
||||||
|
let(:dossier) { create(:dossier, user: user, procedure: procedure) }
|
||||||
|
let(:champ) { dossier.champs.first }
|
||||||
|
|
||||||
|
describe '#update' do
|
||||||
|
render_views
|
||||||
|
before { sign_in user }
|
||||||
|
|
||||||
|
subject do
|
||||||
|
put :update, params: {
|
||||||
|
position: '1',
|
||||||
|
champ_id: champ.id,
|
||||||
|
blob_signed_id: file
|
||||||
|
}, format: 'js'
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when the file is valid' do
|
||||||
|
let(:file) { Rack::Test::UploadedFile.new('spec/fixtures/files/piece_justificative_0.pdf', 'application/pdf') }
|
||||||
|
|
||||||
|
it 'attach the file' do
|
||||||
|
subject
|
||||||
|
champ.reload
|
||||||
|
expect(champ.piece_justificative_file.attached?).to be true
|
||||||
|
expect(champ.piece_justificative_file.filename).to eq('piece_justificative_0.pdf')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'renders the attachment template as Javascript' do
|
||||||
|
subject
|
||||||
|
expect(response.status).to eq(200)
|
||||||
|
expect(response.body).to include("editable-champ[data-champ-id=\"#{champ.id}\"]")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when the file is invalid' do
|
||||||
|
let(:file) { Rack::Test::UploadedFile.new('spec/fixtures/files/invalid_file_format.json', 'application/json') }
|
||||||
|
|
||||||
|
# TODO: for now there are no validators on the champ piece_justificative_file,
|
||||||
|
# so we have to mock a failing validation.
|
||||||
|
# Once the validators will be enabled, remove those mocks, and let the usual
|
||||||
|
# validation fail naturally.
|
||||||
|
#
|
||||||
|
# See https://github.com/betagouv/demarches-simplifiees.fr/issues/4926
|
||||||
|
before do
|
||||||
|
champ
|
||||||
|
expect_any_instance_of(Champs::PieceJustificativeChamp).to receive(:save).and_return(false)
|
||||||
|
expect_any_instance_of(Champs::PieceJustificativeChamp).to receive(:errors)
|
||||||
|
.and_return(double(full_messages: ['La pièce justificative n’est pas d’un type accepté']))
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'doesn’t attach the file' do
|
||||||
|
subject
|
||||||
|
expect(champ.reload.piece_justificative_file.attached?).to be false
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'renders an error' do
|
||||||
|
subject
|
||||||
|
expect(response.status).to eq(422)
|
||||||
|
expect(response.header['Content-Type']).to include('application/json')
|
||||||
|
expect(JSON.parse(response.body)).to eq({ 'errors' => ['La pièce justificative n’est pas d’un type accepté'] })
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -5,9 +5,6 @@ feature 'The user' do
|
||||||
let!(:procedure) { create(:procedure, :published, :for_individual, :with_all_champs_mandatory) }
|
let!(:procedure) { create(:procedure, :published, :for_individual, :with_all_champs_mandatory) }
|
||||||
let(:user_dossier) { user.dossiers.first }
|
let(:user_dossier) { user.dossiers.first }
|
||||||
|
|
||||||
# TODO: check
|
|
||||||
# the order
|
|
||||||
# there are no extraneous input
|
|
||||||
scenario 'fill a dossier', js: true, vcr: { cassette_name: 'api_geo_departements_regions_et_communes' } do
|
scenario 'fill a dossier', js: true, vcr: { cassette_name: 'api_geo_departements_regions_et_communes' } do
|
||||||
log_in(user, procedure)
|
log_in(user, procedure)
|
||||||
|
|
||||||
|
@ -160,6 +157,14 @@ feature 'The user' do
|
||||||
create(:procedure, :published, :for_individual, types_de_champ: tdcs)
|
create(:procedure, :published, :for_individual, types_de_champ: tdcs)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
let(:procedure_with_pjs) do
|
||||||
|
tdcs = [
|
||||||
|
create(:type_de_champ_piece_justificative, mandatory: true, libelle: 'Pièce justificative 1', order_place: 1),
|
||||||
|
create(:type_de_champ_piece_justificative, mandatory: true, libelle: 'Pièce justificative 2', order_place: 2)
|
||||||
|
]
|
||||||
|
create(:procedure, :published, :for_individual, types_de_champ: tdcs)
|
||||||
|
end
|
||||||
|
|
||||||
scenario 'adding, replacing and removing attachments', js: true do
|
scenario 'adding, replacing and removing attachments', js: true do
|
||||||
log_in(user, procedure_with_pj)
|
log_in(user, procedure_with_pj)
|
||||||
fill_individual
|
fill_individual
|
||||||
|
@ -191,6 +196,85 @@ feature 'The user' do
|
||||||
expect(page).to have_no_text('RIB.pdf')
|
expect(page).to have_no_text('RIB.pdf')
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'when the auto-uploads of attachments is enabled' do
|
||||||
|
before do
|
||||||
|
Flipper.enable_actor(:autoupload_dossier_attachments, user)
|
||||||
|
end
|
||||||
|
|
||||||
|
scenario 'add an attachment', js: true do
|
||||||
|
log_in(user, procedure_with_pjs)
|
||||||
|
fill_individual
|
||||||
|
|
||||||
|
# Add attachments
|
||||||
|
find_field('Pièce justificative 1').attach_file(Rails.root + 'spec/fixtures/files/file.pdf')
|
||||||
|
find_field('Pièce justificative 2').attach_file(Rails.root + 'spec/fixtures/files/RIB.pdf')
|
||||||
|
|
||||||
|
# Expect the files to be uploaded immediately
|
||||||
|
expect(page).to have_text('analyse antivirus en cours', count: 2)
|
||||||
|
expect(page).to have_text('file.pdf')
|
||||||
|
expect(page).to have_text('RIB.pdf')
|
||||||
|
|
||||||
|
# Expect the submit buttons to be enabled
|
||||||
|
expect(page).to have_button('Enregistrer le brouillon', disabled: false)
|
||||||
|
expect(page).to have_button('Déposer le dossier', disabled: false)
|
||||||
|
|
||||||
|
# Reload the current page
|
||||||
|
visit current_path
|
||||||
|
|
||||||
|
# Expect the files to have been saved on the dossier
|
||||||
|
expect(page).to have_text('file.pdf')
|
||||||
|
expect(page).to have_text('RIB.pdf')
|
||||||
|
end
|
||||||
|
|
||||||
|
# TODO: once we're running on Rails 6, re-enable the validator on PieceJustificativeChamp,
|
||||||
|
# and unmark this spec as pending.
|
||||||
|
#
|
||||||
|
# See piece_justificative_champ.rb
|
||||||
|
# See https://github.com/betagouv/demarches-simplifiees.fr/issues/4926
|
||||||
|
scenario 'add an invalid attachment', js: true, pending: true do
|
||||||
|
log_in(user, procedure_with_pjs)
|
||||||
|
fill_individual
|
||||||
|
|
||||||
|
# Test invalid file type
|
||||||
|
attach_file('Pièce justificative 1', Rails.root + 'spec/fixtures/files/invalid_file_format.json')
|
||||||
|
expect(page).to have_text('La pièce justificative n’est pas d’un type accepté')
|
||||||
|
expect(page).to have_no_button('Ré-essayer', visible: true)
|
||||||
|
|
||||||
|
# Replace the file by another with a valid type
|
||||||
|
attach_file('Pièce justificative 1', Rails.root + 'spec/fixtures/files/piece_justificative_0.pdf')
|
||||||
|
expect(page).to have_no_text('La pièce justificative n’est pas d’un type accepté')
|
||||||
|
expect(page).to have_text('analyse antivirus en cours')
|
||||||
|
expect(page).to have_text('piece_justificative_0.pdf')
|
||||||
|
end
|
||||||
|
|
||||||
|
scenario 'retry on transcient upload error', js: true do
|
||||||
|
log_in(user, procedure_with_pjs)
|
||||||
|
fill_individual
|
||||||
|
|
||||||
|
# Test auto-upload failure
|
||||||
|
logout(:user) # Make the subsequent auto-upload request fail
|
||||||
|
attach_file('Pièce justificative 1', Rails.root + 'spec/fixtures/files/file.pdf')
|
||||||
|
expect(page).to have_text('Une erreur s’est produite pendant l’envoi du fichier')
|
||||||
|
expect(page).to have_button('Ré-essayer', visible: true)
|
||||||
|
expect(page).to have_button('Enregistrer le brouillon', disabled: false)
|
||||||
|
expect(page).to have_button('Déposer le dossier', disabled: false)
|
||||||
|
|
||||||
|
# Test that retrying after a failure works
|
||||||
|
login_as(user, scope: :user) # Make the auto-upload request work again
|
||||||
|
click_on('Ré-essayer', visible: true)
|
||||||
|
expect(page).to have_text('analyse antivirus en cours')
|
||||||
|
expect(page).to have_text('file.pdf')
|
||||||
|
expect(page).to have_button('Enregistrer le brouillon', disabled: false)
|
||||||
|
expect(page).to have_button('Déposer le dossier', disabled: false)
|
||||||
|
|
||||||
|
# Reload the current page
|
||||||
|
visit current_path
|
||||||
|
|
||||||
|
# Expect the file to have been saved on the dossier
|
||||||
|
expect(page).to have_text('file.pdf')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
context 'when the draft autosave is enabled' do
|
context 'when the draft autosave is enabled' do
|
||||||
before do
|
before do
|
||||||
Flipper.enable_actor(:autosave_dossier_draft, user)
|
Flipper.enable_actor(:autosave_dossier_draft, user)
|
||||||
|
|
3
spec/fixtures/files/invalid_file_format.json
vendored
Normal file
3
spec/fixtures/files/invalid_file_format.json
vendored
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
"text": "The format of this attachment is rejected by most uploaders."
|
||||||
|
}
|
Loading…
Reference in a new issue