Merge pull request #3837 from betagouv/dev

2019-05-03-01
This commit is contained in:
Paul Chavard 2019-05-03 10:58:16 +02:00 committed by GitHub
commit d0026fc887
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
52 changed files with 2442 additions and 3020 deletions

View file

@ -7,25 +7,25 @@ class Champs::SiretController < ApplicationController
find_etablisement
if @siret.empty?
@champ&.update!(value: '')
@etablissement&.destroy
elsif @siret.present? && @siret.length == 14
etablissement = find_etablisement_with_siret
if etablissement.present?
@etablissement = etablissement
return clear_siret_and_etablissement
end
if !@champ.nil?
@champ.update!(value: etablissement.siret, etablissement: etablissement)
end
else
@champ&.update!(value: '')
@etablissement&.destroy
@siret = :not_found
end
else
@champ&.update!(value: '')
@etablissement&.destroy
@siret = :invalid
if @siret.present? && @siret.length != 14
return siret_error(:invalid)
end
begin
etablissement = find_etablissement_with_siret
rescue RestClient::RequestFailed
return siret_error(:network_error)
end
if etablissement.blank?
return siret_error(:not_found)
end
@etablissement = etablissement
if !@champ.nil?
@champ.update!(value: etablissement.siret, etablissement: etablissement)
end
end
@ -49,10 +49,20 @@ class Champs::SiretController < ApplicationController
@procedure_id = @champ&.dossier&.procedure_id || 'aperçu'
end
def find_etablisement_with_siret
def find_etablissement_with_siret
etablissement_attributes = ApiEntrepriseService.get_etablissement_params_for_siret(@siret, @procedure_id)
if etablissement_attributes.present?
Etablissement.new(etablissement_attributes)
end
end
def clear_siret_and_etablissement
@champ&.update!(value: '')
@etablissement&.destroy
end
def siret_error(error)
clear_siret_and_etablissement
@siret = error
end
end

View file

@ -91,7 +91,11 @@ module Users
end
sanitized_siret = siret_model.siret
etablissement_attributes = ApiEntrepriseService.get_etablissement_params_for_siret(sanitized_siret, @dossier.procedure.id)
begin
etablissement_attributes = ApiEntrepriseService.get_etablissement_params_for_siret(sanitized_siret, @dossier.procedure.id)
rescue RestClient::RequestFailed
return render_siret_error(t('errors.messages.siret_network_error'))
end
if etablissement_attributes.blank?
return render_siret_error(t('errors.messages.siret_unknown'))
end
@ -257,7 +261,7 @@ module Users
def show_demarche_en_test_banner
if @dossier.present? && @dossier.procedure.brouillon?
flash.now.notice = "Ce dossier est déposé sur une démarche en test. Il sera supprimé lors de la publication de la démarche."
flash.now.alert = "Ce dossier est déposé sur une démarche en test. Il sera supprimé lors de la publication de la démarche et sa soumission na pas de valeur juridique."
end
end

View file

@ -22,6 +22,7 @@ class TypesDeChampEditor extends Component {
type_champ: 'text',
types_de_champ: [],
private: props.isAnnotation,
drop_down_list_value: '--Premier élément du menu--\n',
libelle: `${
props.isAnnotation ? 'Nouvelle annotation' : 'Nouveau champ'
} ${props.typeDeChampsTypes[0][0]}`

View file

@ -1,64 +1,47 @@
import { CREATE } from 'leaflet-freedraw';
import { delegate } from '@utils';
import {
initMap,
getCurrentMap,
geocodeAddress,
drawCadastre,
drawQuartiersPrioritaires,
drawParcellesAgricoles,
drawUserSelection,
addFreeDrawEvents
} from '../../shared/carte';
import { initMap, drawPolygons, addFreeDrawEvents } from '../../shared/carte';
function initialize() {
for (let element of document.querySelectorAll('.carte')) {
diplayMap(element, null, true);
async function initialize() {
const elements = document.querySelectorAll('.carte');
if (elements.length) {
const editable = [...elements].find(element =>
element.classList.contains('edit')
);
await loadLeaflet(editable);
for (let element of elements) {
diplayMap(element, null, true);
}
}
}
window.DS.drawMapData = (selector, data) => {
let element = document.querySelector(selector);
diplayMap(element, data);
};
// We load leaflet dynamically, ramda and freedraw and assign them to globals.
// Latest freedraw version build needs globals.
async function loadLeaflet(editable) {
window.L = await import('leaflet').then(({ default: L }) => L);
if (editable) {
window.R = await import('ramda').then(({ default: R }) => R);
await import('leaflet-freedraw/dist/leaflet-freedraw.web.js');
}
}
function diplayMap(element, data, initial = false) {
data = data || JSON.parse(element.dataset.geo);
let editable = element.classList.contains('edit');
const editable = element.classList.contains('edit');
const map = initMap(element, data.position, editable);
let map = initMap(element, data.position, editable);
drawPolygons(map, data, { initial, editable });
// draw external polygons
drawCadastre(map, data, editable);
drawQuartiersPrioritaires(map, data, editable);
drawParcellesAgricoles(map, data, editable);
// draw user polygon
if (initial) {
drawUserSelection(map, data, editable);
if (editable) {
let input = element.parentElement.querySelector('input[data-remote]');
addFreeDrawEvents(map, input);
}
if (initial && editable) {
const input = element.parentElement.querySelector('input[data-remote]');
addFreeDrawEvents(map, input);
}
}
addEventListener('turbolinks:load', initialize);
delegate('click', '.toolbar .new-area', event => {
event.preventDefault();
let map = getCurrentMap(event.target);
if (map) {
map.freeDraw.mode(CREATE);
}
});
delegate('autocomplete:select', '.toolbar [data-address]', event => {
let map = getCurrentMap(event.target);
if (map) {
geocodeAddress(map, event.detail.label);
}
addEventListener('carte:update', ({ detail: { selector, data } }) => {
const element = document.querySelector(selector);
diplayMap(element, data);
});

View file

@ -1,10 +1,8 @@
import L from 'leaflet';
import FreeDraw, { NONE, EDIT, DELETE } from 'leaflet-freedraw';
/* globals FreeDraw L */
import { fire, getJSON, delegate } from '@utils';
import polygonArea from './polygon_area';
const LAYERS = {};
const MAPS = new WeakMap();
export function initMap(element, position, editable = false) {
@ -22,7 +20,7 @@ export function initMap(element, position, editable = false) {
if (editable) {
const freeDraw = new FreeDraw({
mode: NONE,
mode: FreeDraw.NONE,
smoothFactor: 4,
mergePolygons: false
});
@ -35,81 +33,39 @@ export function initMap(element, position, editable = false) {
}
}
export function drawCadastre(map, { cadastres }, editable = false) {
drawLayer(
map,
cadastres,
editable ? CADASTRE_POLYGON_STYLE : noEditStyle(CADASTRE_POLYGON_STYLE),
'cadastres'
);
}
export function drawQuartiersPrioritaires(
map,
{ quartiersPrioritaires },
editable = false
) {
drawLayer(
map,
quartiersPrioritaires,
editable ? QP_POLYGON_STYLE : noEditStyle(QP_POLYGON_STYLE),
'quartiersPrioritaires'
);
}
export function drawParcellesAgricoles(
map,
{ parcellesAgricoles },
editable = false
) {
drawLayer(
map,
parcellesAgricoles,
editable ? RPG_POLYGON_STYLE : noEditStyle(RPG_POLYGON_STYLE),
'parcellesAgricoles'
);
export function drawPolygons(map, data, { editable, initial }) {
if (initial) {
drawUserSelection(map, data, editable);
}
clearLayers(map);
drawCadastre(map, data, editable);
drawQuartiersPrioritaires(map, data, editable);
drawParcellesAgricoles(map, data, editable);
bringToFrontUserSelection(map);
}
export function drawUserSelection(map, { selection }, editable = false) {
if (selection) {
const coordinates = toLatLngs(selection);
let polygon;
if (editable) {
coordinates.forEach(polygon => map.freeDraw.create(polygon));
const polygon = map.freeDraw.all()[0];
if (polygon) {
map.fitBounds(polygon.getBounds());
}
[polygon] = markFreeDrawLayers(map);
} else {
const polygon = L.polygon(coordinates, {
polygon = L.polygon(coordinates, {
color: 'red',
zIndex: 3
}).addTo(map);
});
polygon.addTo(map);
}
if (polygon) {
map.fitBounds(polygon.getBounds());
}
}
}
export function geocodeAddress(map, query) {
getJSON('/address/geocode', { request: query }).then(data => {
if (data.lat !== null) {
map.setView(new L.LatLng(data.lat, data.lon), data.zoom);
}
});
}
export function getCurrentMap(input) {
let element = input.closest('.toolbar').parentElement.querySelector('.carte');
if (MAPS.has(element)) {
return MAPS.get(element);
}
}
const EMPTY_GEO_JSON = '[]';
const ERROR_GEO_JSON = '';
export function addFreeDrawEvents(map, selector) {
const input = findInput(selector);
map.freeDraw.on('markers', ({ latLngs }) => {
@ -121,10 +77,65 @@ export function addFreeDrawEvents(map, selector) {
input.value = ERROR_GEO_JSON;
}
markFreeDrawLayers(map);
fire(input, 'change');
});
}
function drawCadastre(map, { cadastres }, editable = false) {
drawLayer(
map,
cadastres,
editable ? CADASTRE_POLYGON_STYLE : noEditStyle(CADASTRE_POLYGON_STYLE)
);
}
function drawQuartiersPrioritaires(
map,
{ quartiersPrioritaires },
editable = false
) {
drawLayer(
map,
quartiersPrioritaires,
editable ? QP_POLYGON_STYLE : noEditStyle(QP_POLYGON_STYLE)
);
}
function drawParcellesAgricoles(map, { parcellesAgricoles }, editable = false) {
drawLayer(
map,
parcellesAgricoles,
editable ? RPG_POLYGON_STYLE : noEditStyle(RPG_POLYGON_STYLE)
);
}
function geocodeAddress(map, query) {
getJSON('/address/geocode', { request: query }).then(data => {
if (data.lat !== null) {
map.setView(new L.LatLng(data.lat, data.lon), data.zoom);
}
});
}
function getCurrentMap(element) {
if (!element.matches('.carte')) {
const closestCarteElement = element.closest('.carte');
const closestToolbarElement = element.closest('.toolbar');
element = closestCarteElement
? closestCarteElement
: closestToolbarElement.parentElement.querySelector('.carte');
}
if (MAPS.has(element)) {
return MAPS.get(element);
}
}
const EMPTY_GEO_JSON = '[]';
const ERROR_GEO_JSON = '';
function toLatLngs({ coordinates }) {
return coordinates.map(polygon =>
polygon[0].map(point => ({ lng: point[0], lat: point[1] }))
@ -137,28 +148,40 @@ function findInput(selector) {
: selector;
}
function createLayer(map, layerName) {
const layer = (LAYERS[layerName] = new L.GeoJSON(undefined, {
function createLayer(map) {
const layer = new L.GeoJSON(undefined, {
interactive: false
}));
});
layer.addTo(map);
return layer;
}
function removeLayer(map, layerName) {
const layer = LAYERS[layerName];
if (layer) {
delete LAYERS[layerName];
map.removeLayer(layer);
}
function clearLayers(map) {
map.eachLayer(layer => {
if (layer instanceof L.GeoJSON) {
map.removeLayer(layer);
}
});
}
function drawLayer(map, data, style, layerName = 'default') {
removeLayer(map, layerName);
function bringToFrontUserSelection(map) {
map.eachLayer(layer => {
if (layer.isFreeDraw) {
layer.bringToFront();
}
});
}
function markFreeDrawLayers(map) {
return map.freeDraw.all().map(layer => {
layer.isFreeDraw = true;
return layer;
});
}
function drawLayer(map, data, style) {
if (Array.isArray(data) && data.length > 0) {
const layer = createLayer(map, layerName);
const layer = createLayer(map);
data.forEach(function(item) {
layer.addData(item.geometry);
@ -197,18 +220,33 @@ const RPG_POLYGON_STYLE = Object.assign({}, POLYGON_STYLE, {
});
delegate('click', '.carte.edit', event => {
let element = event.target;
let isPath = element.matches('.leaflet-container g path');
let map = element.matches('.carte') ? element : element.closest('.carte');
let freeDraw = MAPS.has(map) ? MAPS.get(map).freeDraw : null;
const map = getCurrentMap(event.target);
if (freeDraw) {
if (map) {
const isPath = event.target.matches('.leaflet-container g path');
if (isPath) {
setTimeout(() => {
freeDraw.mode(EDIT | DELETE);
map.freeDraw.mode(FreeDraw.EDIT | FreeDraw.DELETE);
}, 50);
} else {
freeDraw.mode(NONE);
map.freeDraw.mode(FreeDraw.NONE);
}
}
});
delegate('click', '.toolbar .new-area', event => {
event.preventDefault();
const map = getCurrentMap(event.target);
if (map) {
map.freeDraw.mode(FreeDraw.CREATE);
}
});
delegate('autocomplete:select', '.toolbar [data-address]', event => {
const map = getCurrentMap(event.target);
if (map) {
geocodeAddress(map, event.detail.label);
}
});

View file

@ -1,20 +0,0 @@
class AntiVirusJob < ApplicationJob
include ActiveStorage::Downloading
attr_reader :blob
def perform(virus_scan)
@blob = ActiveStorage::Blob.find_by(key: virus_scan.blob_key)
if @blob.present?
download_blob_to_tempfile do |file|
if ClamavService.safe_file?(file.path)
status = VirusScan.statuses.fetch(:safe)
else
status = VirusScan.statuses.fetch(:infected)
end
virus_scan.update(scanned_at: Time.zone.now, status: status)
end
end
end
end

View file

@ -2,7 +2,11 @@ class EtablissementUpdateJob < ApplicationJob
queue_as :default
def perform(dossier, siret)
etablissement_attributes = ApiEntrepriseService.get_etablissement_params_for_siret(siret, dossier.procedure_id)
begin
etablissement_attributes = ApiEntrepriseService.get_etablissement_params_for_siret(siret, dossier.procedure_id)
rescue
return
end
if etablissement_attributes.present?
if dossier.etablissement.present?

View file

@ -0,0 +1,10 @@
class VirusScannerJob < ApplicationJob
def perform(blob)
metadata = extract_metadata_via_virus_scanner(blob)
blob.update!(metadata: blob.metadata.merge(metadata))
end
def extract_metadata_via_virus_scanner(blob)
ActiveStorage::VirusScanner.new(blob).metadata
end
end

View file

@ -0,0 +1,46 @@
class ActiveStorage::VirusScanner
include ActiveStorage::Downloading
def initialize(blob)
@blob = blob
end
attr_reader :blob
PENDING = 'pending'
INFECTED = 'infected'
SAFE = 'safe'
def pending?
blob.metadata[:virus_scan_result] == PENDING
end
def infected?
blob.metadata[:virus_scan_result] == INFECTED
end
def safe?
blob.metadata[:virus_scan_result] == SAFE
end
def analyzed?
blob.metadata[:virus_scan_result].present?
end
def analyze_later
if !analyzed?
blob.update!(metadata: blob.metadata.merge(virus_scan_result: PENDING))
VirusScannerJob.perform_later(blob)
end
end
def metadata
download_blob_to_tempfile do |file|
if ClamavService.safe_file?(file.path)
{ virus_scan_result: SAFE, scanned_at: Time.zone.now }
else
{ virus_scan_result: INFECTED, scanned_at: Time.zone.now }
end
end
end
end

View file

@ -7,9 +7,11 @@ class ApiEntreprise::Adapter
end
def data_source
@data_source ||= get_resource
rescue
@data_source = nil
begin
@data_source ||= get_resource
rescue RestClient::ResourceNotFound
@data_source = nil
end
end
def to_params

View file

@ -34,13 +34,21 @@ class ApiEntreprise::API
if response.success?
JSON.parse(response.body, symbolize_names: true)
else
elsif response.code == 404 || response.code == 422
raise RestClient::ResourceNotFound
else
raise RestClient::RequestFailed
end
end
def self.url(resource_name, siret_or_siren)
[API_ENTREPRISE_URL, resource_name, siret_or_siren].join("/")
base_url = [API_ENTREPRISE_URL, resource_name, siret_or_siren].join("/")
if Flipflop.insee_api_v3?
base_url += "?with_insee_v3=true"
end
base_url
end
def self.params(siret_or_siren, procedure_id)

View file

@ -1,6 +1,5 @@
class Avis < ApplicationRecord
include EmailSanitizableConcern
include VirusScanConcern
belongs_to :dossier, touch: true
belongs_to :gestionnaire
@ -22,9 +21,6 @@ class Avis < ApplicationRecord
scope :by_latest, -> { order(updated_at: :desc) }
scope :updated_since?, -> (date) { where('avis.updated_at > ?', date) }
after_commit :create_avis_virus_scan
after_initialize { add_virus_scan_on(self.piece_justificative_file) }
# The form allows subtmitting avis requests to several emails at once,
# hence this virtual attribute.
attr_accessor :emails
@ -41,6 +37,27 @@ class Avis < ApplicationRecord
Avis.find_by(id: avis_id)&.email == email
end
# FIXME remove this after migrating virus_scan to blob metadata
def virus_scan
VirusScan.find_by(blob_key: piece_justificative_file.blob.key)
end
def virus_scan_safe?
virus_scan&.safe? || piece_justificative_file.virus_scanner.safe?
end
def virus_scan_infected?
virus_scan&.infected? || piece_justificative_file.virus_scanner.infected?
end
def virus_scan_pending?
virus_scan&.pending? || piece_justificative_file.virus_scanner.pending?
end
def virus_scan_no_scan?
virus_scan.blank? && !piece_justificative_file.virus_scanner.analyzed?
end
private
def notify_gestionnaire
@ -54,8 +71,4 @@ class Avis < ApplicationRecord
self.email = nil
end
end
def create_avis_virus_scan
create_virus_scan(self.piece_justificative_file)
end
end

View file

@ -1,6 +1,4 @@
class Champs::PieceJustificativeChamp < Champ
after_commit :create_virus_scan
PIECE_JUSTIFICATIVE_FILE_MAX_SIZE = 200.megabytes
PIECE_JUSTIFICATIVE_FILE_ACCEPTED_FORMATS = [
@ -48,20 +46,26 @@ class Champs::PieceJustificativeChamp < Champ
errors
end
# FIXME remove this after migrating virus_scan to blob metadata
def virus_scan_safe?
virus_scan&.safe? || piece_justificative_file.virus_scanner.safe?
end
def virus_scan_infected?
virus_scan&.infected? || piece_justificative_file.virus_scanner.infected?
end
def virus_scan_pending?
virus_scan&.pending? || piece_justificative_file.virus_scanner.pending?
end
def virus_scan_no_scan?
virus_scan.blank? && !piece_justificative_file.virus_scanner.analyzed?
end
def for_api
if piece_justificative_file.attached? && (virus_scan&.safe? || virus_scan&.pending?)
if piece_justificative_file.attached? && (virus_scan_safe? || virus_scan_pending?)
Rails.application.routes.url_helpers.url_for(piece_justificative_file)
end
end
private
def create_virus_scan
if self.piece_justificative_file&.attachment&.blob.present?
VirusScan.where(champ: self).where.not(blob_key: self.piece_justificative_file.blob.key).delete_all
VirusScan.find_or_create_by!(champ: self, blob_key: self.piece_justificative_file.blob.key) do |virus_scan|
virus_scan.status = VirusScan.statuses.fetch(:pending)
end
end
end
end

View file

@ -1,21 +0,0 @@
module VirusScanConcern
extend ActiveSupport::Concern
attr_reader :attachment_attribute
def add_virus_scan_on(piece_justificative)
@attachment_attribute = piece_justificative
end
def virus_scan
VirusScan.find_by(blob_key: self.attachment_attribute.blob.key)
end
def create_virus_scan(piece_justificative)
if piece_justificative&.attachment&.blob.present?
VirusScan.find_or_create_by!(blob_key: piece_justificative.blob.key) do |virus_scan|
virus_scan.status = VirusScan.statuses.fetch(:pending)
end
end
end
end

View file

@ -346,14 +346,6 @@ class Procedure < ApplicationRecord
percentile_time(:en_construction_at, :processed_at, 90)
end
def usual_verification_time
percentile_time(:en_construction_at, :en_instruction_at, 90)
end
def usual_instruction_time
percentile_time(:en_instruction_at, :processed_at, 90)
end
PATH_AVAILABLE = :available
PATH_AVAILABLE_PUBLIEE = :available_publiee
PATH_NOT_AVAILABLE = :not_available

View file

@ -6,12 +6,4 @@ class VirusScan < ApplicationRecord
safe: 'safe',
infected: 'infected'
}
validates :champ_id, uniqueness: { scope: :blob_key }
after_create :perform_scan
def perform_scan
AntiVirusJob.perform_later(self)
end
end

View file

@ -1,4 +1,11 @@
class ApiEntrepriseService
# Retrieve all informations we can get about a SIRET.
#
# Returns nil if the SIRET is unknown; and nested params
# suitable for being saved into a Etablissement object otherwise.
#
# Raises a RestClient::RequestFailed exception on transcient errors
# (timeout, 5XX HTTP error code, etc.)
def self.get_etablissement_params_for_siret(siret, procedure_id)
etablissement_params = ApiEntreprise::EtablissementAdapter.new(siret, procedure_id).to_params
entreprise_params = ApiEntreprise::EntrepriseAdapter.new(siret, procedure_id).to_params

View file

@ -4,4 +4,4 @@
partial: 'shared/champs/carte/geo_areas',
locals: { champ: @champ, error: @error }) %>
DS.drawMapData("<%= @selector %>", <%= geo_data(@champ) %>);
<%= fire_event('carte:update', { selector: @selector, data: @champ.to_render_data }.to_json) %>

View file

@ -14,7 +14,8 @@
= favicon_link_tag(image_url("favicons/32x32.png"), type: "image/png", sizes: "32x32")
= favicon_link_tag(image_url("favicons/96x96.png"), type: "image/png", sizes: "96x96")
= javascript_pack_tag 'application', defer: true, 'data-turbolinks-track': 'reload'
- packs = ['application', 'track', administrateur_signed_in? ? 'sendinblue' : nil].compact
= javascript_packs_with_chunks_tag *packs, defer: true, 'data-turbolinks-track': 'reload'
= stylesheet_link_tag 'new_design/new_application', media: 'all', 'data-turbolinks-track': 'reload'
= stylesheet_link_tag 'new_design/print', media: 'print', 'data-turbolinks-track': 'reload'
@ -42,7 +43,3 @@
= javascript_include_tag :xray
= yield :charts_js
= javascript_pack_tag 'track', async: true
- if administrateur_signed_in?
= javascript_pack_tag 'sendinblue', async: true

View file

@ -12,7 +12,8 @@
= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': "reload"
= stylesheet_link_tag 'print', media: 'print', 'data-turbolinks-track': "reload"
= javascript_pack_tag 'application-old', defer: true, 'data-turbolinks-track': 'reload'
- packs = ['application-old', 'track', administrateur_signed_in? ? 'sendinblue' : nil].compact
= javascript_packs_with_chunks_tag *packs, defer: true, 'data-turbolinks-track': 'reload'
= javascript_include_tag 'application', defer: true, 'data-turbolinks-track': 'reload'
= csrf_meta_tags
= Gon::Base.render_data(camel_case: true, init: true)
@ -42,6 +43,5 @@
%i.fa.fa-times{ style: 'position: fixed; top: 10; right: 30; color: white;' }
= render partial: 'layouts/footer', locals: { main_container_size: main_container_size }
= javascript_pack_tag 'track', async: true
- if administrateur_signed_in?
= javascript_pack_tag 'sendinblue', async: true

View file

@ -1,21 +0,0 @@
- pj = champ.piece_justificative_file
.pj-link
- if champ.virus_scan.blank? || champ.virus_scan.safe?
= link_to url_for(pj), target: '_blank', rel: 'noopener', title: "Télécharger la pièce jointe" do
%span.icon.attachment
= pj.filename.to_s
- if champ.virus_scan.blank?
(ce fichier na pas été analysé par notre antivirus, téléchargez-le avec précaution)
- else
= pj.filename.to_s
- if champ.virus_scan.pending?
(analyse antivirus en cours
= link_to "rafraichir", request.path
)
- elsif champ.virus_scan.infected?
- if user_can_upload
(virus détecté, merci denvoyer un autre fichier)
- else
(virus détecté, le téléchargement de ce fichier est bloqué)

View file

@ -1,5 +1,5 @@
- pj = champ.piece_justificative_file
- if pj.attached?
= render partial: "shared/champs/piece_justificative/pj_link", locals: { champ: champ, user_can_upload: false }
= render partial: "shared/piece_jointe/pj_link", locals: { object: champ, pj: pj, user_can_upload: false }
- else
Pièce justificative non fournie

View file

@ -6,6 +6,9 @@
Nous navons pas trouvé détablissement correspondant à ce numéro de SIRET.
= link_to('Plus dinformations', "https://faq.demarches-simplifiees.fr/article/4-erreur-siret", target: '_blank', rel: 'noopener')
- when :network_error
= t('errors.messages.siret_network_error')
- else
- if siret.present? && siret == etablissement&.siret
= render partial: 'shared/dossiers/editable_champs/etablissement_titre', locals: { etablissement: etablissement }

View file

@ -9,7 +9,7 @@
- if pj.attached?
.piece-justificative-actions{ id: "piece_justificative_#{champ.id}" }
.piece-justificative-action
= render partial: "shared/champs/piece_justificative/pj_link", locals: { champ: champ, user_can_upload: true }
= render partial: "shared/piece_jointe/pj_link", locals: { object: champ, pj: champ.piece_justificative_file, user_can_upload: true }
.piece-justificative-action
- if champ.private?
= link_to 'Supprimer', gestionnaire_champ_purge_champ_piece_justificative_path(procedure_id: champ.dossier.procedure_id, dossier_id: champ.dossier_id, champ_id: champ.id), remote: true, method: :delete, class: 'button small danger'

View file

@ -1,19 +1,19 @@
- if pj.attached?
.pj-link
- if object.virus_scan.blank? || object.virus_scan.safe?
= link_to url_for(pj), target: '_blank', title: "Télécharger la pièce jointe" do
- if object.virus_scan_safe? || object.virus_scan_no_scan?
= link_to url_for(pj), target: '_blank', rel: 'noopener', title: "Télécharger la pièce jointe" do
%span.icon.attachment
= pj.filename.to_s
- if object.virus_scan.blank?
- if object.virus_scan_no_scan?
(ce fichier na pas été analysé par notre antivirus, téléchargez-le avec précaution)
- else
= pj.filename.to_s
- if object.virus_scan.pending?
- if object.virus_scan_pending?
(analyse antivirus en cours
= link_to "rafraichir", request.path
)
- elsif object.virus_scan.infected?
- elsif object.virus_scan_infected?
- if user_can_upload
(virus détecté, merci denvoyer un autre fichier)
- else

View file

@ -0,0 +1,9 @@
/ FIXME: remove the custom procedure switch at some point
- procedure_id_for_which_we_hide_the_estimated_delay = 6547
- procedure_path_for_which_we_hide_the_estimated_delay = 'deposer-une-offre-de-stage'
- show_time_means = procedure.id != procedure_id_for_which_we_hide_the_estimated_delay && procedure.path != procedure_path_for_which_we_hide_the_estimated_delay
- cache(procedure.id, expires_in: 1.day) do
- if procedure.usual_traitement_time && show_time_means
%p
Habituellement, les dossiers de cette démarche sont traités dans un délai de #{distance_of_time_in_words(procedure.usual_traitement_time)}.

View file

@ -1,6 +1,4 @@
- procedure_id_for_which_we_hide_the_time_means = 6547
- procedure_path_for_which_we_hide_the_time_means = 'deposer-une-offre-de-stage'
- show_time_means = dossier.procedure.id != procedure_id_for_which_we_hide_the_time_means && dossier.procedure.path != procedure_path_for_which_we_hide_the_time_means
.status-overview
- if !dossier.termine?
@ -24,17 +22,11 @@
- elsif dossier.en_construction?
.en-construction
%p Un instructeur de ladministration est en train de vérifier que votre dossier est bien complet. Si des modifications sont nécessaires, vous recevrez un message avec les modifications à effectuer.
%p
Sinon,
= succeed '.' do
%strong votre dossier passera directement en instruction
/ FIXME: remove the custom procedure switch at some point
- if dossier.procedure.usual_verification_time && show_time_means
- cache(dossier.procedure, expires_in: 1.week) do
%p
Habituellement, les dossiers de cette démarche sont vérifiés dans un délai de #{distance_of_time_in_words(dossier.procedure.usual_verification_time)}.
= render partial: 'users/dossiers/show/estimated_delay', locals: { procedure: dossier.procedure }
- elsif dossier.en_instruction?
.en-instruction
@ -44,12 +36,7 @@
%strong
vous recevrez un email
avec le résultat.
/ FIXME: remove the custom procedure switch at some point
- if dossier.procedure.usual_instruction_time && show_time_means
- cache(dossier.procedure, expires_in: 1.week) do
%p
Habituellement, les dossiers de cette démarche sont traités dans un délai de #{distance_of_time_in_words(dossier.procedure.usual_instruction_time)}.
= render partial: 'users/dossiers/show/estimated_delay', locals: { procedure: dossier.procedure }
- elsif dossier.accepte?
.accepte

View file

@ -20,6 +20,8 @@ Flipflop.configure do
group :production do
feature :remote_storage,
default: ENV['FOG_ENABLED'] == 'enabled'
feature :insee_api_v3,
default: true
feature :weekly_overview,
default: ENV['APP_NAME'] == 'tps'
feature :pre_maintenance_mode

View file

@ -1 +1,29 @@
ActiveStorage::Service.url_expires_in = 1.hour
# We want to run the virus scan on every ActiveStorage attachment,
# regardless of its type (user-uploaded document, instructeur-uploaded attestation, form template, etc.)
#
# To do this, the best place to work on is the ActiveStorage::Attachment
# objects themselves.
#
# We have to monkey patch ActiveStorage in order to always run an analyzer.
# The way analyzable blob interface work is by running the first accepted analyzer.
# This is not what we want for the virus scan. Using analyzer interface is still beneficial
# as it takes care of downloading the blob.
ActiveStorage::Attachment.class_eval do
after_create_commit :virus_scan
private
def virus_scan
ActiveStorage::VirusScanner.new(blob).analyze_later
end
end
ActiveStorage::Attached::One.class_eval do
def virus_scanner
if attached?
ActiveStorage::VirusScanner.new(attachment.blob)
end
end
end

View file

@ -182,7 +182,8 @@ fr:
dossier_map_not_activated: "Le dossier n'a pas accès à la cartographie."
invalid_siret: "Le siret est incorrect"
procedure_not_found: "La démarche n'existe pas"
siret_unknown: 'Désolé, nous navons pas trouvé détablissement enregistré correspondant à ce numéro SIRET'
siret_unknown: 'Désolé, nous navons pas trouvé détablissement enregistré correspondant à ce numéro SIRET.'
siret_network_error: 'Désolé, la récupération des informations SIRET est temporairement indisponible. Veuillez réessayer dans quelques instants.'
etablissement_fail: 'Désolé, nous navons pas réussi à enregistrer létablissement correspondant à ce numéro SIRET'
france_connect:
connexion: "Erreur lors de la connexion à France Connect."

View file

@ -7,5 +7,6 @@ const resolve = {
}
};
environment.splitChunks();
environment.config.merge({ resolve });
module.exports = environment;

View file

@ -2,8 +2,4 @@ process.env.NODE_ENV = process.env.NODE_ENV || 'production';
const environment = require('./environment');
// https://github.com/rails/webpacker/issues/1235
environment.config.optimization.minimizer[0].options.uglifyOptions.ecma = 5; // for IE 11 support
environment.config.optimization.minimizer[0].options.uglifyOptions.safari10 = true;
module.exports = environment.toWebpackConfig();

View file

@ -0,0 +1,22 @@
namespace :after_party do
desc 'Deployment task: migrate_virus_scans'
task migrate_virus_scans: :environment do
puts "Running deploy task 'migrate_virus_scans'"
virus_scans = VirusScan.all
progress = ProgressReport.new(virus_scans.count)
virus_scans.find_each do |virus_scan|
blob = ActiveStorage::Blob.find_by(key: virus_scan.blob_key)
if blob
metadata = { virus_scan_result: virus_scan.status, scanned_at: virus_scan.scanned_at }
blob.update_column(:metadata, blob.metadata.merge(metadata))
end
progress.inc
end
progress.finish
# Update task as completed. If you remove the line below, the task will
# run with every deploy (or every time you call after_party:run).
AfterParty::TaskRecord.create version: '20190425102459'
end
end

View file

@ -1,44 +1,44 @@
{
"dependencies": {
"@babel/preset-react": "^7.0.0",
"@fortawesome/fontawesome-svg-core": "^1.2.15",
"@fortawesome/free-solid-svg-icons": "^5.7.2",
"@fortawesome/fontawesome-svg-core": "^1.2.17",
"@fortawesome/free-solid-svg-icons": "^5.8.1",
"@fortawesome/react-fontawesome": "^0.1.4",
"@rails/webpacker": "4.0.0-pre.3",
"@sentry/browser": "^4.6.5",
"@rails/webpacker": "4.0.2",
"@sentry/browser": "^5.1.0",
"@turf/area": "^6.0.1",
"activestorage": "^5.2.2",
"activestorage": "^5.2.3",
"autocomplete.js": "^0.36.0",
"babel-plugin-macros": "^2.5.0",
"babel-plugin-macros": "^2.5.1",
"babel-plugin-transform-react-remove-prop-types": "^0.4.24",
"chartkick": "^3.0.1",
"chartkick": "^3.0.2",
"debounce": "^1.2.0",
"dom4": "^2.1.3",
"dom4": "^2.1.4",
"highcharts": "^6.1.2",
"jquery": "^3.3.1",
"leaflet-freedraw": "^2.9.0",
"leaflet": "^1.3.4",
"jquery": "^3.4.0",
"leaflet": "^1.4.0",
"leaflet-freedraw": "^2.10.0",
"prop-types": "^15.7.2",
"rails-ujs": "^5.2.2",
"ramda": "^0.25.0",
"react_ujs": "^2.4.4",
"react-dom": "^16.8.4",
"rails-ujs": "^5.2.3",
"ramda": "=0.24.1",
"react": "^16.8.6",
"react-dom": "^16.8.6",
"react-scroll-to-component": "^1.0.2",
"react-sortable-hoc": "^1.7.1",
"react": "^16.8.4",
"react_ujs": "^2.5.0",
"select2": "^4.0.6-rc.1",
"turbolinks": "^5.2.0"
},
"devDependencies": {
"babel-eslint": "^10.0.1",
"eclint": "^2.8.0",
"eslint": "^5.9.0",
"eslint-config-prettier": "^3.3.0",
"eslint-plugin-prettier": "^3.0.0",
"eclint": "^2.8.1",
"eslint": "^5.16.0",
"eslint-config-prettier": "^4.1.0",
"eslint-plugin-prettier": "^3.0.1",
"eslint-plugin-react": "^7.12.4",
"eslint-plugin-react-hooks": "^1.5.1",
"prettier": "^1.15.3",
"webpack-dev-server": "^3.1.9"
"eslint-plugin-react-hooks": "^1.6.0",
"prettier": "^1.17.0",
"webpack-dev-server": "^3.3.1"
},
"scripts": {
"lint:ec": "eclint check $({ git ls-files ; find vendor -type f ; echo 'db/schema.rb' ; } | sort | uniq -u)",

View file

@ -45,7 +45,7 @@ describe Champs::CarteController, type: :controller do
expect(assigns(:error)).to eq(nil)
expect(champ.reload.value).to eq(nil)
expect(champ.reload.geo_areas).to eq([])
expect(response.body).to include("DS.drawMapData(\".carte-1\", {\"position\":{\"lon\":\"2.428462\",\"lat\":\"46.538192\",\"zoom\":\"13\"},\"selection\":null,\"quartiersPrioritaires\":[],\"cadastres\":[],\"parcellesAgricoles\":[]});")
expect(response.body).to include("DS.fire('carte:update', {\"selector\":\".carte-1\",\"data\":{\"position\":{\"lon\":\"2.428462\",\"lat\":\"46.538192\",\"zoom\":\"13\"},\"selection\":null,\"quartiersPrioritaires\":[],\"cadastres\":[],\"parcellesAgricoles\":[]}});")
}
end

View file

@ -20,74 +20,105 @@ describe Champs::SiretController, type: :controller do
end
let(:siret) { '' }
context 'when user is connected' do
context 'when the user is signed in' do
render_views
before { sign_in user }
context 'when siret empty' do
before {
get :show, params: params, format: 'js'
}
context 'when the SIRET is empty' do
subject! { get :show, params: params, format: 'js' }
it 'empty info message' do
it 'clears the etablissement and SIRET on the model' do
champ.reload
expect(champ.etablissement).to be_nil
expect(champ.value).to be_empty
end
it 'clears any information or error message' do
expect(response.body).to include('.siret-info-1')
expect(response.body).to include('innerHTML = ""')
champ.reload
expect(champ.etablissement).to be_nil
expect(champ.value).to be_empty
end
end
context 'when siret invalid' do
context 'when the SIRET is invalid' do
let(:siret) { '1234' }
before {
get :show, params: params, format: 'js'
}
it 'invalid error' do
subject! { get :show, params: params, format: 'js' }
it 'clears the etablissement and SIRET on the model' do
champ.reload
expect(champ.etablissement).to be_nil
expect(champ.value).to be_empty
end
it 'displays a “SIRET is invalid” error message' do
expect(response.body).to include('Le numéro de SIRET doit comporter exactement 14 chiffres.')
end
end
context 'when the API is unavailable' do
let(:siret) { '82161143100015' }
before do
allow(controller).to receive(:find_etablissement_with_siret).and_raise(RestClient::RequestFailed)
end
subject! { get :show, params: params, format: 'js' }
it 'clears the etablissement and SIRET on the model' do
champ.reload
expect(champ.etablissement).to be_nil
expect(champ.value).to be_empty
end
it 'displays a “API is unavailable” error message' do
expect(response.body).to include(I18n.t('errors.messages.siret_network_error'))
end
end
context 'when siret not found' do
let(:siret) { '0' * 14 }
before {
expect(subject).to receive(:find_etablisement_with_siret).and_return(false)
get :show, params: params, format: 'js'
}
context 'when the SIRET is valid but unknown' do
let(:siret) { '00000000000000' }
it 'not found error' do
before do
allow(controller).to receive(:find_etablissement_with_siret).and_return(false)
end
subject! { get :show, params: params, format: 'js' }
it 'clears the etablissement and SIRET on the model' do
champ.reload
expect(champ.etablissement).to be_nil
expect(champ.value).to be_empty
end
it 'displays a “SIRET not found” error message' do
expect(response.body).to include('Nous navons pas trouvé détablissement correspondant à ce numéro de SIRET.')
champ.reload
expect(champ.etablissement).to be_nil
expect(champ.value).to be_empty
end
end
context 'when siret found' do
context 'when the SIRET informations are retrieved successfully' do
let(:siret) { etablissement.siret }
let(:etablissement) { build(:etablissement) }
before {
expect(subject).to receive(:find_etablisement_with_siret).and_return(etablissement)
get :show, params: params, format: 'js'
}
it 'etablissement info message' do
expect(response.body).to include(etablissement.entreprise_raison_sociale)
before do
allow(controller).to receive(:find_etablissement_with_siret).and_return(etablissement)
end
subject! { get :show, params: params, format: 'js' }
it 'populates the etablissement and SIRET on the model' do
champ.reload
expect(champ.value).to eq(etablissement.siret)
expect(champ.etablissement.siret).to eq(etablissement.siret)
end
it 'displays the name of the company' do
expect(response.body).to include(etablissement.entreprise_raison_sociale)
end
end
end
context 'when user is not connected' do
before {
get :show, params: { position: '1' }, format: 'js'
}
context 'when user is not signed in' do
subject! { get :show, params: { position: '1' }, format: 'js' }
it { expect(response.code).to eq('401') }
end

View file

@ -278,6 +278,13 @@ describe Users::DossiersController, type: :controller do
context 'with a valid SIRET' do
let(:params_siret) { '440 117 620 01530' }
context 'When API-Entreprise is down' do
let(:api_etablissement_status) { 502 }
let(:api_body_status) { File.read('spec/fixtures/files/api_entreprise/exercices_unavailable.json') }
it_behaves_like 'the request fails with an error', I18n.t('errors.messages.siret_network_error')
end
context 'when API-Entreprise doesnt know this SIRET' do
let(:api_etablissement_status) { 404 }
let(:api_body_status) { '' }

View file

@ -167,8 +167,8 @@ feature 'The user' do
expect(page).to have_text('analyse antivirus en cours')
# Mark file as scanned and clean
virus_scan = VirusScan.last
virus_scan.update(scanned_at: Time.zone.now, status: VirusScan.statuses.fetch(:safe))
attachment = ActiveStorage::Attachment.last
attachment.blob.update(metadata: attachment.blob.metadata.merge(scanned_at: Time.zone.now, virus_scan_result: ActiveStorage::VirusScanner::SAFE))
within '.piece-justificative' do
click_on 'rafraichir'
end

View file

@ -17,28 +17,24 @@ describe 'Dossier details:' do
end
describe "the user can see the mean time they are expected to wait" do
context "when the dossier is in construction" do
before do
other_dossier = create(:dossier, :accepte, :for_individual, procedure: procedure, en_construction_at: 10.days.ago, en_instruction_at: Time.zone.now)
visit dossier_path(dossier)
end
let(:other_dossier) { create(:dossier, :accepte, :for_individual, procedure: procedure, en_construction_at: 10.days.ago, en_instruction_at: 9.days.ago, processed_at: Time.zone.now) }
it { expect(page).to have_text("Habituellement, les dossiers de cette démarche sont vérifiés dans un délai de 10 jours.") }
context "when the dossier is in construction" do
it "displays the estimated wait duration" do
other_dossier
visit dossier_path(dossier)
expect(page).to have_text("Habituellement, les dossiers de cette démarche sont traités dans un délai de 10 jours.")
end
end
context "when the dossier is in instruction" do
let(:dossier) { create(:dossier, :en_instruction, :for_individual, :with_commentaires, user: user, procedure: procedure) }
before do
Timecop.freeze(Time.zone.local(2012, 12, 20))
other_dossier = create(:dossier, :accepte, :for_individual, procedure: procedure, en_instruction_at: 60.days.ago, processed_at: Time.zone.now)
it "displays the estimated wait duration" do
other_dossier
visit dossier_path(dossier)
expect(page).to have_text("Habituellement, les dossiers de cette démarche sont traités dans un délai de 10 jours.")
end
after { Timecop.return }
it { expect(page).to have_text("Habituellement, les dossiers de cette démarche sont traités dans un délai de 2 mois.") }
end
end

View file

@ -0,0 +1,7 @@
{
"errors": [
"Erreur interne du serveur",
"Le siret ou siren indiqué n'existe pas, n'est pas connu ou ne comporte aucune information pour cet appel"
],
"gateway_error": true
}

View file

@ -1,12 +1,11 @@
RSpec.describe AntiVirusJob, type: :job do
RSpec.describe VirusScannerJob, type: :job do
let(:champ) do
champ = create(:champ, :piece_justificative)
champ.piece_justificative_file.attach(io: StringIO.new("toto"), filename: "toto.txt", content_type: "text/plain")
champ
end
let(:virus_scan) { create(:virus_scan, status: VirusScan.statuses.fetch(:pending), champ: champ, blob_key: champ.piece_justificative_file.blob.key) }
subject { AntiVirusJob.new.perform(virus_scan) }
subject { VirusScannerJob.new.perform(champ.piece_justificative_file.blob) }
context "when no virus is found" do
let(:virus_found?) { true }
@ -16,7 +15,7 @@ RSpec.describe AntiVirusJob, type: :job do
subject
end
it { expect(virus_scan.reload.status).to eq(VirusScan.statuses.fetch(:safe)) }
it { expect(champ.piece_justificative_file.virus_scanner.safe?).to be_truthy }
end
context "when a virus is found" do
@ -27,6 +26,6 @@ RSpec.describe AntiVirusJob, type: :job do
subject
end
it { expect(virus_scan.reload.status).to eq(VirusScan.statuses.fetch(:infected)) }
it { expect(champ.piece_justificative_file.virus_scanner.infected?).to be_truthy }
end
end

View file

@ -2,21 +2,35 @@ require 'spec_helper'
describe ApiEntreprise::API do
let(:procedure_id) { 12 }
describe '.entreprise' do
subject { described_class.entreprise(siren, procedure_id) }
before do
stub_request(:get, /https:\/\/entreprise.api.gouv.fr\/v2\/entreprises\/#{siren}?.*token=/)
.to_return(status: status, body: body)
end
context 'when the service is unavailable' do
let(:siren) { '111111111' }
let(:status) { 502 }
let(:body) { File.read('spec/fixtures/files/api_entreprise/entreprises_unavailable.json') }
it 'raises RestClient::RequestFailed' do
expect { subject }.to raise_error(RestClient::RequestFailed)
end
end
context 'when siren does not exist' do
let(:siren) { '111111111' }
let(:status) { 404 }
let(:body) { '' }
let(:body) { File.read('spec/fixtures/files/api_entreprise/entreprises_not_found.json') }
it 'raises RestClient::ResourceNotFound' do
expect { subject }.to raise_error(RestClient::ResourceNotFound)
end
end
context 'when siret exist' do
let(:siren) { '418166096' }
let(:status) { 200 }

View file

@ -11,7 +11,7 @@ describe ApiEntreprise::EntrepriseAdapter do
.to_return(body: body, status: status)
end
context "when SIRET is OK" do
context "when the SIRET is valid" do
let(:body) { File.read('spec/fixtures/files/api_entreprise/entreprises.json') }
let(:status) { 200 }
@ -70,12 +70,21 @@ describe ApiEntreprise::EntrepriseAdapter do
end
end
context "when SIRET is KO" do
context "when the SIRET is unknown" do
let(:body) { File.read('spec/fixtures/files/api_entreprise/entreprises_not_found.json') }
let(:status) { 206 }
let(:status) { 404 }
it '#to_params class est une Hash ?' do
expect(subject).to eq({})
end
end
context "when the service is unavailable" do
let(:body) { File.read('spec/fixtures/files/api_entreprise/entreprises_unavailable.json') }
let(:status) { 502 }
it 'raises an exception' do
expect { subject }.to raise_error(RestClient::RequestFailed)
end
end
end

View file

@ -10,25 +10,15 @@ describe ApiEntreprise::ExercicesAdapter do
.to_return(body: File.read('spec/fixtures/files/api_entreprise/exercices.json', status: 200))
end
it '#to_params class est un Hash ?' do
expect(subject).to be_an_instance_of(Hash)
end
it { is_expected.to be_an_instance_of(Hash) }
it 'have 3 exercices' do
it 'contains several exercices attributes' do
expect(subject[:exercices_attributes].size).to eq(3)
end
context 'Attributs Exercices' do
it 'L\'exercice contient bien un ca' do
expect(subject[:exercices_attributes][0][:ca]).to eq('21009417')
end
it 'L\'exercice contient bien une date de fin d\'exercice' do
expect(subject[:exercices_attributes][0][:date_fin_exercice]).to eq("2013-12-31T00:00:00+01:00")
end
it 'L\'exercice contient bien une date_fin_exercice_timestamp' do
expect(subject[:exercices_attributes][0][:date_fin_exercice_timestamp]).to eq(1388444400)
end
it 'contains informations in each exercices_attributes' do
expect(subject[:exercices_attributes][0][:ca]).to eq('21009417')
expect(subject[:exercices_attributes][0][:date_fin_exercice]).to eq("2013-12-31T00:00:00+01:00")
expect(subject[:exercices_attributes][0][:date_fin_exercice_timestamp]).to eq(1388444400)
end
end

View file

@ -17,7 +17,7 @@ describe ApiEntreprise::RNAAdapter do
context 'when siret is not valid' do
let(:siret) { '234567' }
let(:body) { '' }
let(:status) { '404' }
let(:status) { 404 }
it { is_expected.to eq({}) }
end

View file

@ -383,21 +383,20 @@ describe Champ do
let(:type_de_champ) { create(:type_de_champ_piece_justificative) }
context 'and there is a blob' do
before { champ.piece_justificative_file.attach(io: StringIO.new("toto"), filename: "toto.txt", content_type: "text/plain") }
before do
champ.piece_justificative_file.attach(io: StringIO.new("toto"), filename: "toto.txt", content_type: "text/plain")
champ.save
end
it { expect { champ.save }.to change(VirusScan, :count).by(1) }
it { expect(champ.piece_justificative_file.virus_scanner.analyzed?).to be_truthy }
end
context 'and there is no blob' do
it { expect { champ.save }.to_not change(VirusScan, :count) }
before { champ.save }
it { expect(champ.piece_justificative_file.virus_scanner).to be_nil }
end
end
context 'when type_champ is not type_de_champ_piece_justificative' do
let(:type_de_champ) { create(:type_de_champ_textarea) }
it { expect { champ.save }.to_not change(VirusScan, :count) }
end
end
describe "repetition" do

View file

@ -1,23 +1,24 @@
describe Champs::PieceJustificativeChamp do
describe '#for_api' do
let(:champ_pj) { create(:champ_piece_justificative) }
let(:metadata) { champ_pj.piece_justificative_file.blob.metadata }
before { champ_pj.virus_scan.update(status: status) }
before { champ_pj.piece_justificative_file.blob.update(metadata: metadata.merge(virus_scan_result: status)) }
subject { champ_pj.for_api }
context 'when file is safe' do
let(:status) { 'safe' }
let(:status) { ActiveStorage::VirusScanner::SAFE }
it { is_expected.to include("/rails/active_storage/blobs/") }
end
context 'when file is not scanned' do
let(:status) { 'pending' }
let(:status) { ActiveStorage::VirusScanner::PENDING }
it { is_expected.to include("/rails/active_storage/blobs/") }
end
context 'when file is infected' do
let(:status) { 'infected' }
let(:status) { ActiveStorage::VirusScanner::INFECTED }
it { is_expected.to be_nil }
end
end

View file

@ -727,59 +727,17 @@ describe Procedure do
end
end
describe '#usual_verification_time' do
describe '#usual_traitement_time' do
let(:procedure) { create(:procedure) }
def create_dossier(construction_date:, instruction_date:)
dossier = create(:dossier, :en_instruction, procedure: procedure)
dossier.update!(en_construction_at: construction_date, en_instruction_at: instruction_date)
end
before do
delays.each do |delay|
create_dossier(construction_date: 1.week.ago - delay, instruction_date: 1.week.ago)
end
end
context 'when there are several dossiers in the time frame' do
let(:delays) { [1.day, 2.days, 2.days, 2.days, 2.days, 3.days, 3.days, 3.days, 3.days, 12.days] }
it 'returns a time representative of the dossier verification delay' do
expect(procedure.usual_verification_time).to be_between(3.days, 4.days)
end
end
context 'when there are very old dossiers' do
let(:delays) { [2.days, 2.days] }
let!(:old_dossier) { create_dossier(construction_date: 3.months.ago, instruction_date: 2.months.ago) }
it 'ignores dossiers older than 1 month' do
expect(procedure.usual_verification_time).to be_within(1.hour).of(2.days)
end
end
context 'when there is only one dossier in the time frame' do
let(:delays) { [1.day] }
it { expect(procedure.usual_verification_time).to be_within(1.hour).of(1.day) }
end
context 'where there are no dossiers' do
let(:delays) { [] }
it { expect(procedure.usual_verification_time).to be_nil }
end
end
describe '#usual_instruction_time' do
let(:procedure) { create(:procedure) }
def create_dossier(instruction_date:, processed_date:)
def create_dossier(construction_date:, instruction_date:, processed_date:)
dossier = create(:dossier, :accepte, procedure: procedure)
dossier.update!(en_instruction_at: instruction_date, processed_at: processed_date)
dossier.update!(en_construction_at: construction_date, en_instruction_at: instruction_date, processed_at: processed_date)
end
before do
delays.each do |delay|
create_dossier(instruction_date: 1.week.ago - delay, processed_date: 1.week.ago)
create_dossier(construction_date: 1.week.ago - delay, instruction_date: 1.week.ago - delay + 12.hours, processed_date: 1.week.ago)
end
end
@ -787,36 +745,36 @@ describe Procedure do
let(:delays) { [1.day, 2.days, 2.days, 2.days, 2.days, 3.days, 3.days, 3.days, 3.days, 12.days] }
it 'returns a time representative of the dossier instruction delay' do
expect(procedure.usual_instruction_time).to be_between(3.days, 4.days)
expect(procedure.usual_traitement_time).to be_between(3.days, 4.days)
end
end
context 'when there are very old dossiers' do
let(:delays) { [2.days, 2.days] }
let!(:old_dossier) { create_dossier(instruction_date: 3.months.ago, processed_date: 2.months.ago) }
let!(:old_dossier) { create_dossier(construction_date: 3.months.ago, instruction_date: 2.months.ago, processed_date: 2.months.ago) }
it 'ignores dossiers older than 1 month' do
expect(procedure.usual_instruction_time).to be_within(1.hour).of(2.days)
expect(procedure.usual_traitement_time).to be_within(1.hour).of(2.days)
end
end
context 'when there is a dossier with bad data' do
let(:delays) { [2.days, 2.days] }
let!(:bad_dossier) { create_dossier(instruction_date: nil, processed_date: 10.days.ago) }
let!(:bad_dossier) { create_dossier(construction_date: nil, instruction_date: nil, processed_date: 10.days.ago) }
it 'ignores bad dossiers' do
expect(procedure.usual_instruction_time).to be_within(1.hour).of(2.days)
expect(procedure.usual_traitement_time).to be_within(1.hour).of(2.days)
end
end
context 'when there is only one processed dossier' do
let(:delays) { [1.day] }
it { expect(procedure.usual_instruction_time).to be_within(1.hour).of(1.day) }
it { expect(procedure.usual_traitement_time).to be_within(1.hour).of(1.day) }
end
context 'where there is no processed dossier' do
let(:delays) { [] }
it { expect(procedure.usual_instruction_time).to be_nil }
it { expect(procedure.usual_traitement_time).to be_nil }
end
end

View file

@ -10,7 +10,7 @@ describe ChampSerializer do
before do
champ.piece_justificative_file.attach({ filename: __FILE__, io: File.open(__FILE__) })
champ.reload.virus_scan.safe!
champ.piece_justificative_file.virus_scanner.analyze_later
end
after { champ.piece_justificative_file.purge }

View file

@ -1,19 +1,17 @@
require 'rails_helper'
describe 'shared/champs/piece_justificative/_pj_link.html.haml', type: :view do
let(:champ) { create(:champ, :piece_justificative, :with_piece_justificative_file) }
let(:virus_scan) { nil }
describe 'shared/piece_jointe/_pj_link.html.haml', type: :view do
let(:champ) { create(:champ_piece_justificative) }
let(:virus_scan_result) { nil }
before do
if virus_scan
champ.update(virus_scan: virus_scan)
end
champ.piece_justificative_file.blob.update(metadata: champ.piece_justificative_file.blob.metadata.merge(virus_scan_result: virus_scan_result))
end
subject { render 'shared/champs/piece_justificative/pj_link', champ: champ, user_can_upload: false }
subject { render 'shared/piece_jointe/pj_link', object: champ, pj: champ.piece_justificative_file, user_can_upload: false }
context 'when there is no anti-virus scan' do
let(:virus_scan) { nil }
let(:virus_scan_result) { nil }
it 'allows to download the file' do
expect(subject).to have_link(champ.piece_justificative_file.filename.to_s)
@ -22,7 +20,7 @@ describe 'shared/champs/piece_justificative/_pj_link.html.haml', type: :view do
end
context 'when the anti-virus scan is pending' do
let(:virus_scan) { create(:virus_scan, :pending) }
let(:virus_scan_result) { ActiveStorage::VirusScanner::PENDING }
it 'displays the filename, but doesnt allow to download the file' do
expect(subject).to have_text(champ.piece_justificative_file.filename.to_s)
@ -32,7 +30,7 @@ describe 'shared/champs/piece_justificative/_pj_link.html.haml', type: :view do
end
context 'when the file is scanned and safe' do
let(:virus_scan) { create(:virus_scan, :safe) }
let(:virus_scan_result) { ActiveStorage::VirusScanner::SAFE }
it 'allows to download the file' do
expect(subject).to have_link(champ.piece_justificative_file.filename.to_s)
@ -40,7 +38,7 @@ describe 'shared/champs/piece_justificative/_pj_link.html.haml', type: :view do
end
context 'when the file is scanned and infected' do
let(:virus_scan) { create(:virus_scan, :infected) }
let(:virus_scan_result) { ActiveStorage::VirusScanner::INFECTED }
it 'displays the filename, but doesnt allow to download the file' do
expect(subject).to have_text(champ.piece_justificative_file.filename.to_s)

View file

@ -1,4 +1,6 @@
describe 'users/dossiers/show/_status_overview.html.haml', type: :view do
before { allow(dossier.procedure).to receive(:usual_traitement_time).and_return(1.day) }
subject! { render 'users/dossiers/show/status_overview.html.haml', dossier: dossier }
matcher :have_timeline_item do |selector|
@ -46,6 +48,7 @@ describe 'users/dossiers/show/_status_overview.html.haml', type: :view do
end
it { is_expected.to have_selector('.status-explanation .en-construction') }
it { is_expected.to have_text('Habituellement, les dossiers de cette démarche sont traités dans un délai de 1 jour') }
end
context 'when en instruction' do
@ -59,6 +62,7 @@ describe 'users/dossiers/show/_status_overview.html.haml', type: :view do
end
it { is_expected.to have_selector('.status-explanation .en-instruction') }
it { is_expected.to have_text('Habituellement, les dossiers de cette démarche sont traités dans un délai de 1 jour') }
end
context 'when accepté' do

4405
yarn.lock

File diff suppressed because it is too large Load diff