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

View file

@ -91,7 +91,11 @@ module Users
end end
sanitized_siret = siret_model.siret 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? if etablissement_attributes.blank?
return render_siret_error(t('errors.messages.siret_unknown')) return render_siret_error(t('errors.messages.siret_unknown'))
end end
@ -257,7 +261,7 @@ module Users
def show_demarche_en_test_banner def show_demarche_en_test_banner
if @dossier.present? && @dossier.procedure.brouillon? 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
end end

View file

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

View file

@ -1,64 +1,47 @@
import { CREATE } from 'leaflet-freedraw'; import { initMap, drawPolygons, addFreeDrawEvents } from '../../shared/carte';
import { delegate } from '@utils';
import {
initMap,
getCurrentMap,
geocodeAddress,
drawCadastre,
drawQuartiersPrioritaires,
drawParcellesAgricoles,
drawUserSelection,
addFreeDrawEvents
} from '../../shared/carte';
function initialize() { async function initialize() {
for (let element of document.querySelectorAll('.carte')) { const elements = document.querySelectorAll('.carte');
diplayMap(element, null, true);
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) => { // We load leaflet dynamically, ramda and freedraw and assign them to globals.
let element = document.querySelector(selector); // Latest freedraw version build needs globals.
diplayMap(element, data); 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) { function diplayMap(element, data, initial = false) {
data = data || JSON.parse(element.dataset.geo); 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 if (initial && editable) {
drawCadastre(map, data, editable); const input = element.parentElement.querySelector('input[data-remote]');
drawQuartiersPrioritaires(map, data, editable); addFreeDrawEvents(map, input);
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);
}
} }
} }
addEventListener('turbolinks:load', initialize); addEventListener('turbolinks:load', initialize);
delegate('click', '.toolbar .new-area', event => { addEventListener('carte:update', ({ detail: { selector, data } }) => {
event.preventDefault(); const element = document.querySelector(selector);
let map = getCurrentMap(event.target); diplayMap(element, data);
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);
}
}); });

View file

@ -1,10 +1,8 @@
import L from 'leaflet'; /* globals FreeDraw L */
import FreeDraw, { NONE, EDIT, DELETE } from 'leaflet-freedraw';
import { fire, getJSON, delegate } from '@utils'; import { fire, getJSON, delegate } from '@utils';
import polygonArea from './polygon_area'; import polygonArea from './polygon_area';
const LAYERS = {};
const MAPS = new WeakMap(); const MAPS = new WeakMap();
export function initMap(element, position, editable = false) { export function initMap(element, position, editable = false) {
@ -22,7 +20,7 @@ export function initMap(element, position, editable = false) {
if (editable) { if (editable) {
const freeDraw = new FreeDraw({ const freeDraw = new FreeDraw({
mode: NONE, mode: FreeDraw.NONE,
smoothFactor: 4, smoothFactor: 4,
mergePolygons: false mergePolygons: false
}); });
@ -35,81 +33,39 @@ export function initMap(element, position, editable = false) {
} }
} }
export function drawCadastre(map, { cadastres }, editable = false) { export function drawPolygons(map, data, { editable, initial }) {
drawLayer( if (initial) {
map, drawUserSelection(map, data, editable);
cadastres, }
editable ? CADASTRE_POLYGON_STYLE : noEditStyle(CADASTRE_POLYGON_STYLE), clearLayers(map);
'cadastres' drawCadastre(map, data, editable);
); drawQuartiersPrioritaires(map, data, editable);
} drawParcellesAgricoles(map, data, editable);
bringToFrontUserSelection(map);
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 drawUserSelection(map, { selection }, editable = false) { export function drawUserSelection(map, { selection }, editable = false) {
if (selection) { if (selection) {
const coordinates = toLatLngs(selection); const coordinates = toLatLngs(selection);
let polygon;
if (editable) { if (editable) {
coordinates.forEach(polygon => map.freeDraw.create(polygon)); coordinates.forEach(polygon => map.freeDraw.create(polygon));
const polygon = map.freeDraw.all()[0]; [polygon] = markFreeDrawLayers(map);
if (polygon) {
map.fitBounds(polygon.getBounds());
}
} else { } else {
const polygon = L.polygon(coordinates, { polygon = L.polygon(coordinates, {
color: 'red', color: 'red',
zIndex: 3 zIndex: 3
}).addTo(map); });
polygon.addTo(map);
}
if (polygon) {
map.fitBounds(polygon.getBounds()); 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) { export function addFreeDrawEvents(map, selector) {
const input = findInput(selector); const input = findInput(selector);
map.freeDraw.on('markers', ({ latLngs }) => { map.freeDraw.on('markers', ({ latLngs }) => {
@ -121,10 +77,65 @@ export function addFreeDrawEvents(map, selector) {
input.value = ERROR_GEO_JSON; input.value = ERROR_GEO_JSON;
} }
markFreeDrawLayers(map);
fire(input, 'change'); 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 }) { function toLatLngs({ coordinates }) {
return coordinates.map(polygon => return coordinates.map(polygon =>
polygon[0].map(point => ({ lng: point[0], lat: point[1] })) polygon[0].map(point => ({ lng: point[0], lat: point[1] }))
@ -137,28 +148,40 @@ function findInput(selector) {
: selector; : selector;
} }
function createLayer(map, layerName) { function createLayer(map) {
const layer = (LAYERS[layerName] = new L.GeoJSON(undefined, { const layer = new L.GeoJSON(undefined, {
interactive: false interactive: false
})); });
layer.addTo(map); layer.addTo(map);
return layer; return layer;
} }
function removeLayer(map, layerName) { function clearLayers(map) {
const layer = LAYERS[layerName]; map.eachLayer(layer => {
if (layer instanceof L.GeoJSON) {
if (layer) { map.removeLayer(layer);
delete LAYERS[layerName]; }
map.removeLayer(layer); });
}
} }
function drawLayer(map, data, style, layerName = 'default') { function bringToFrontUserSelection(map) {
removeLayer(map, layerName); 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) { if (Array.isArray(data) && data.length > 0) {
const layer = createLayer(map, layerName); const layer = createLayer(map);
data.forEach(function(item) { data.forEach(function(item) {
layer.addData(item.geometry); layer.addData(item.geometry);
@ -197,18 +220,33 @@ const RPG_POLYGON_STYLE = Object.assign({}, POLYGON_STYLE, {
}); });
delegate('click', '.carte.edit', event => { delegate('click', '.carte.edit', event => {
let element = event.target; const map = getCurrentMap(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;
if (freeDraw) { if (map) {
const isPath = event.target.matches('.leaflet-container g path');
if (isPath) { if (isPath) {
setTimeout(() => { setTimeout(() => {
freeDraw.mode(EDIT | DELETE); map.freeDraw.mode(FreeDraw.EDIT | FreeDraw.DELETE);
}, 50); }, 50);
} else { } 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 queue_as :default
def perform(dossier, siret) 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 etablissement_attributes.present?
if dossier.etablissement.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 end
def data_source def data_source
@data_source ||= get_resource begin
rescue @data_source ||= get_resource
@data_source = nil rescue RestClient::ResourceNotFound
@data_source = nil
end
end end
def to_params def to_params

View file

@ -34,13 +34,21 @@ class ApiEntreprise::API
if response.success? if response.success?
JSON.parse(response.body, symbolize_names: true) JSON.parse(response.body, symbolize_names: true)
else elsif response.code == 404 || response.code == 422
raise RestClient::ResourceNotFound raise RestClient::ResourceNotFound
else
raise RestClient::RequestFailed
end end
end end
def self.url(resource_name, siret_or_siren) 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 end
def self.params(siret_or_siren, procedure_id) def self.params(siret_or_siren, procedure_id)

View file

@ -1,6 +1,5 @@
class Avis < ApplicationRecord class Avis < ApplicationRecord
include EmailSanitizableConcern include EmailSanitizableConcern
include VirusScanConcern
belongs_to :dossier, touch: true belongs_to :dossier, touch: true
belongs_to :gestionnaire belongs_to :gestionnaire
@ -22,9 +21,6 @@ class Avis < ApplicationRecord
scope :by_latest, -> { order(updated_at: :desc) } scope :by_latest, -> { order(updated_at: :desc) }
scope :updated_since?, -> (date) { where('avis.updated_at > ?', date) } 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, # The form allows subtmitting avis requests to several emails at once,
# hence this virtual attribute. # hence this virtual attribute.
attr_accessor :emails attr_accessor :emails
@ -41,6 +37,27 @@ class Avis < ApplicationRecord
Avis.find_by(id: avis_id)&.email == email Avis.find_by(id: avis_id)&.email == email
end 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 private
def notify_gestionnaire def notify_gestionnaire
@ -54,8 +71,4 @@ class Avis < ApplicationRecord
self.email = nil self.email = nil
end end
end end
def create_avis_virus_scan
create_virus_scan(self.piece_justificative_file)
end
end end

View file

@ -1,6 +1,4 @@
class Champs::PieceJustificativeChamp < Champ class Champs::PieceJustificativeChamp < Champ
after_commit :create_virus_scan
PIECE_JUSTIFICATIVE_FILE_MAX_SIZE = 200.megabytes PIECE_JUSTIFICATIVE_FILE_MAX_SIZE = 200.megabytes
PIECE_JUSTIFICATIVE_FILE_ACCEPTED_FORMATS = [ PIECE_JUSTIFICATIVE_FILE_ACCEPTED_FORMATS = [
@ -48,20 +46,26 @@ class Champs::PieceJustificativeChamp < Champ
errors errors
end 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 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) Rails.application.routes.url_helpers.url_for(piece_justificative_file)
end end
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 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) percentile_time(:en_construction_at, :processed_at, 90)
end 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 = :available
PATH_AVAILABLE_PUBLIEE = :available_publiee PATH_AVAILABLE_PUBLIEE = :available_publiee
PATH_NOT_AVAILABLE = :not_available PATH_NOT_AVAILABLE = :not_available

View file

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

View file

@ -1,4 +1,11 @@
class ApiEntrepriseService 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) def self.get_etablissement_params_for_siret(siret, procedure_id)
etablissement_params = ApiEntreprise::EtablissementAdapter.new(siret, procedure_id).to_params etablissement_params = ApiEntreprise::EtablissementAdapter.new(siret, procedure_id).to_params
entreprise_params = ApiEntreprise::EntrepriseAdapter.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', partial: 'shared/champs/carte/geo_areas',
locals: { champ: @champ, error: @error }) %> 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/32x32.png"), type: "image/png", sizes: "32x32")
= favicon_link_tag(image_url("favicons/96x96.png"), type: "image/png", sizes: "96x96") = 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/new_application', media: 'all', 'data-turbolinks-track': 'reload'
= stylesheet_link_tag 'new_design/print', media: 'print', 'data-turbolinks-track': 'reload' = stylesheet_link_tag 'new_design/print', media: 'print', 'data-turbolinks-track': 'reload'
@ -42,7 +43,3 @@
= javascript_include_tag :xray = javascript_include_tag :xray
= yield :charts_js = 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 'application', media: 'all', 'data-turbolinks-track': "reload"
= stylesheet_link_tag 'print', media: 'print', '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' = javascript_include_tag 'application', defer: true, 'data-turbolinks-track': 'reload'
= csrf_meta_tags = csrf_meta_tags
= Gon::Base.render_data(camel_case: true, init: true) = 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;' } %i.fa.fa-times{ style: 'position: fixed; top: 10; right: 30; color: white;' }
= render partial: 'layouts/footer', locals: { main_container_size: main_container_size } = render partial: 'layouts/footer', locals: { main_container_size: main_container_size }
= javascript_pack_tag 'track', async: true
- if administrateur_signed_in? - if administrateur_signed_in?
= javascript_pack_tag 'sendinblue', async: true = 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 - pj = champ.piece_justificative_file
- if pj.attached? - 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 - else
Pièce justificative non fournie Pièce justificative non fournie

View file

@ -6,6 +6,9 @@
Nous navons pas trouvé détablissement correspondant à ce numéro de SIRET. 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') = 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 - else
- if siret.present? && siret == etablissement&.siret - if siret.present? && siret == etablissement&.siret
= render partial: 'shared/dossiers/editable_champs/etablissement_titre', locals: { etablissement: etablissement } = render partial: 'shared/dossiers/editable_champs/etablissement_titre', locals: { etablissement: etablissement }

View file

@ -9,7 +9,7 @@
- if pj.attached? - if pj.attached?
.piece-justificative-actions{ id: "piece_justificative_#{champ.id}" } .piece-justificative-actions{ id: "piece_justificative_#{champ.id}" }
.piece-justificative-action .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 .piece-justificative-action
- if champ.private? - 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' = 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? - if pj.attached?
.pj-link .pj-link
- if object.virus_scan.blank? || object.virus_scan.safe? - if object.virus_scan_safe? || object.virus_scan_no_scan?
= link_to url_for(pj), target: '_blank', title: "Télécharger la pièce jointe" do = link_to url_for(pj), target: '_blank', rel: 'noopener', title: "Télécharger la pièce jointe" do
%span.icon.attachment %span.icon.attachment
= pj.filename.to_s = 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) (ce fichier na pas été analysé par notre antivirus, téléchargez-le avec précaution)
- else - else
= pj.filename.to_s = pj.filename.to_s
- if object.virus_scan.pending? - if object.virus_scan_pending?
(analyse antivirus en cours (analyse antivirus en cours
= link_to "rafraichir", request.path = link_to "rafraichir", request.path
) )
- elsif object.virus_scan.infected? - elsif object.virus_scan_infected?
- if user_can_upload - if user_can_upload
(virus détecté, merci denvoyer un autre fichier) (virus détecté, merci denvoyer un autre fichier)
- else - 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 .status-overview
- if !dossier.termine? - if !dossier.termine?
@ -24,17 +22,11 @@
- elsif dossier.en_construction? - elsif dossier.en_construction?
.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 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 %p
Sinon, Sinon,
= succeed '.' do = succeed '.' do
%strong votre dossier passera directement en instruction %strong votre dossier passera directement en instruction
= render partial: 'users/dossiers/show/estimated_delay', locals: { procedure: dossier.procedure }
/ 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)}.
- elsif dossier.en_instruction? - elsif dossier.en_instruction?
.en-instruction .en-instruction
@ -44,12 +36,7 @@
%strong %strong
vous recevrez un email vous recevrez un email
avec le résultat. avec le résultat.
= render partial: 'users/dossiers/show/estimated_delay', locals: { procedure: dossier.procedure }
/ 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)}.
- elsif dossier.accepte? - elsif dossier.accepte?
.accepte .accepte

View file

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

View file

@ -1 +1,29 @@
ActiveStorage::Service.url_expires_in = 1.hour 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." dossier_map_not_activated: "Le dossier n'a pas accès à la cartographie."
invalid_siret: "Le siret est incorrect" invalid_siret: "Le siret est incorrect"
procedure_not_found: "La démarche n'existe pas" 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' etablissement_fail: 'Désolé, nous navons pas réussi à enregistrer létablissement correspondant à ce numéro SIRET'
france_connect: france_connect:
connexion: "Erreur lors de la connexion à France Connect." connexion: "Erreur lors de la connexion à France Connect."

View file

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

View file

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

View file

@ -20,74 +20,105 @@ describe Champs::SiretController, type: :controller do
end end
let(:siret) { '' } let(:siret) { '' }
context 'when user is connected' do context 'when the user is signed in' do
render_views render_views
before { sign_in user } before { sign_in user }
context 'when siret empty' do context 'when the SIRET is empty' do
before { subject! { get :show, params: params, format: 'js' }
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('.siret-info-1')
expect(response.body).to include('innerHTML = ""') expect(response.body).to include('innerHTML = ""')
champ.reload
expect(champ.etablissement).to be_nil
expect(champ.value).to be_empty
end end
end end
context 'when siret invalid' do context 'when the SIRET is invalid' do
let(:siret) { '1234' } 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.') 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 champ.reload
expect(champ.etablissement).to be_nil expect(champ.etablissement).to be_nil
expect(champ.value).to be_empty expect(champ.value).to be_empty
end end
it 'displays a “API is unavailable” error message' do
expect(response.body).to include(I18n.t('errors.messages.siret_network_error'))
end
end end
context 'when siret not found' do context 'when the SIRET is valid but unknown' do
let(:siret) { '0' * 14 } let(:siret) { '00000000000000' }
before {
expect(subject).to receive(:find_etablisement_with_siret).and_return(false)
get :show, params: params, format: 'js'
}
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.') 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
end end
context 'when siret found' do context 'when the SIRET informations are retrieved successfully' do
let(:siret) { etablissement.siret } let(:siret) { etablissement.siret }
let(:etablissement) { build(:etablissement) } 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 before do
expect(response.body).to include(etablissement.entreprise_raison_sociale) 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 champ.reload
expect(champ.value).to eq(etablissement.siret) expect(champ.value).to eq(etablissement.siret)
expect(champ.etablissement.siret).to eq(etablissement.siret) expect(champ.etablissement.siret).to eq(etablissement.siret)
end end
it 'displays the name of the company' do
expect(response.body).to include(etablissement.entreprise_raison_sociale)
end
end end
end end
context 'when user is not connected' do context 'when user is not signed in' do
before { subject! { get :show, params: { position: '1' }, format: 'js' }
get :show, params: { position: '1' }, format: 'js'
}
it { expect(response.code).to eq('401') } it { expect(response.code).to eq('401') }
end end

View file

@ -278,6 +278,13 @@ describe Users::DossiersController, type: :controller do
context 'with a valid SIRET' do context 'with a valid SIRET' do
let(:params_siret) { '440 117 620 01530' } 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 context 'when API-Entreprise doesnt know this SIRET' do
let(:api_etablissement_status) { 404 } let(:api_etablissement_status) { 404 }
let(:api_body_status) { '' } let(:api_body_status) { '' }

View file

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

View file

@ -17,28 +17,24 @@ describe 'Dossier details:' do
end end
describe "the user can see the mean time they are expected to wait" do describe "the user can see the mean time they are expected to wait" do
context "when the dossier is in construction" do 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) }
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
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 end
context "when the dossier is in instruction" do context "when the dossier is in instruction" do
let(:dossier) { create(:dossier, :en_instruction, :for_individual, :with_commentaires, user: user, procedure: procedure) } let(:dossier) { create(:dossier, :en_instruction, :for_individual, :with_commentaires, user: user, procedure: procedure) }
before do it "displays the estimated wait duration" do
Timecop.freeze(Time.zone.local(2012, 12, 20)) other_dossier
other_dossier = create(:dossier, :accepte, :for_individual, procedure: procedure, en_instruction_at: 60.days.ago, processed_at: Time.zone.now)
visit dossier_path(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
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
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 let(:champ) do
champ = create(:champ, :piece_justificative) champ = create(:champ, :piece_justificative)
champ.piece_justificative_file.attach(io: StringIO.new("toto"), filename: "toto.txt", content_type: "text/plain") champ.piece_justificative_file.attach(io: StringIO.new("toto"), filename: "toto.txt", content_type: "text/plain")
champ champ
end 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 context "when no virus is found" do
let(:virus_found?) { true } let(:virus_found?) { true }
@ -16,7 +15,7 @@ RSpec.describe AntiVirusJob, type: :job do
subject subject
end 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 end
context "when a virus is found" do context "when a virus is found" do
@ -27,6 +26,6 @@ RSpec.describe AntiVirusJob, type: :job do
subject subject
end 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
end end

View file

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

View file

@ -11,7 +11,7 @@ describe ApiEntreprise::EntrepriseAdapter do
.to_return(body: body, status: status) .to_return(body: body, status: status)
end 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(:body) { File.read('spec/fixtures/files/api_entreprise/entreprises.json') }
let(:status) { 200 } let(:status) { 200 }
@ -70,12 +70,21 @@ describe ApiEntreprise::EntrepriseAdapter do
end end
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(: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 it '#to_params class est une Hash ?' do
expect(subject).to eq({}) expect(subject).to eq({})
end end
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 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)) .to_return(body: File.read('spec/fixtures/files/api_entreprise/exercices.json', status: 200))
end end
it '#to_params class est un Hash ?' do it { is_expected.to be_an_instance_of(Hash) }
expect(subject).to be_an_instance_of(Hash)
end
it 'have 3 exercices' do it 'contains several exercices attributes' do
expect(subject[:exercices_attributes].size).to eq(3) expect(subject[:exercices_attributes].size).to eq(3)
end end
context 'Attributs Exercices' do it 'contains informations in each exercices_attributes' do
it 'L\'exercice contient bien un ca' do expect(subject[:exercices_attributes][0][:ca]).to eq('21009417')
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")
end expect(subject[:exercices_attributes][0][:date_fin_exercice_timestamp]).to eq(1388444400)
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
end end
end end

View file

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

View file

@ -383,21 +383,20 @@ describe Champ do
let(:type_de_champ) { create(:type_de_champ_piece_justificative) } let(:type_de_champ) { create(:type_de_champ_piece_justificative) }
context 'and there is a blob' do 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 end
context 'and there is no blob' do 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
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 end
describe "repetition" do describe "repetition" do

View file

@ -1,23 +1,24 @@
describe Champs::PieceJustificativeChamp do describe Champs::PieceJustificativeChamp do
describe '#for_api' do describe '#for_api' do
let(:champ_pj) { create(:champ_piece_justificative) } 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 } subject { champ_pj.for_api }
context 'when file is safe' do context 'when file is safe' do
let(:status) { 'safe' } let(:status) { ActiveStorage::VirusScanner::SAFE }
it { is_expected.to include("/rails/active_storage/blobs/") } it { is_expected.to include("/rails/active_storage/blobs/") }
end end
context 'when file is not scanned' do context 'when file is not scanned' do
let(:status) { 'pending' } let(:status) { ActiveStorage::VirusScanner::PENDING }
it { is_expected.to include("/rails/active_storage/blobs/") } it { is_expected.to include("/rails/active_storage/blobs/") }
end end
context 'when file is infected' do context 'when file is infected' do
let(:status) { 'infected' } let(:status) { ActiveStorage::VirusScanner::INFECTED }
it { is_expected.to be_nil } it { is_expected.to be_nil }
end end
end end

View file

@ -727,59 +727,17 @@ describe Procedure do
end end
end end
describe '#usual_verification_time' do describe '#usual_traitement_time' do
let(:procedure) { create(:procedure) } let(:procedure) { create(:procedure) }
def create_dossier(construction_date:, instruction_date:) def create_dossier(construction_date:, instruction_date:, processed_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:)
dossier = create(:dossier, :accepte, procedure: procedure) 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 end
before do before do
delays.each do |delay| 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
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] } 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 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
end end
context 'when there are very old dossiers' do context 'when there are very old dossiers' do
let(:delays) { [2.days, 2.days] } 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 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
end end
context 'when there is a dossier with bad data' do context 'when there is a dossier with bad data' do
let(:delays) { [2.days, 2.days] } 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 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
end end
context 'when there is only one processed dossier' do context 'when there is only one processed dossier' do
let(:delays) { [1.day] } 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 end
context 'where there is no processed dossier' do context 'where there is no processed dossier' do
let(:delays) { [] } let(:delays) { [] }
it { expect(procedure.usual_instruction_time).to be_nil } it { expect(procedure.usual_traitement_time).to be_nil }
end end
end end

View file

@ -10,7 +10,7 @@ describe ChampSerializer do
before do before do
champ.piece_justificative_file.attach({ filename: __FILE__, io: File.open(__FILE__) }) 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 end
after { champ.piece_justificative_file.purge } after { champ.piece_justificative_file.purge }

View file

@ -1,19 +1,17 @@
require 'rails_helper' require 'rails_helper'
describe 'shared/champs/piece_justificative/_pj_link.html.haml', type: :view do describe 'shared/piece_jointe/_pj_link.html.haml', type: :view do
let(:champ) { create(:champ, :piece_justificative, :with_piece_justificative_file) } let(:champ) { create(:champ_piece_justificative) }
let(:virus_scan) { nil } let(:virus_scan_result) { nil }
before do before do
if virus_scan champ.piece_justificative_file.blob.update(metadata: champ.piece_justificative_file.blob.metadata.merge(virus_scan_result: virus_scan_result))
champ.update(virus_scan: virus_scan)
end
end 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 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 it 'allows to download the file' do
expect(subject).to have_link(champ.piece_justificative_file.filename.to_s) 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 end
context 'when the anti-virus scan is pending' do 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 it 'displays the filename, but doesnt allow to download the file' do
expect(subject).to have_text(champ.piece_justificative_file.filename.to_s) 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 end
context 'when the file is scanned and safe' do 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 it 'allows to download the file' do
expect(subject).to have_link(champ.piece_justificative_file.filename.to_s) 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 end
context 'when the file is scanned and infected' do 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 it 'displays the filename, but doesnt allow to download the file' do
expect(subject).to have_text(champ.piece_justificative_file.filename.to_s) 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 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 } subject! { render 'users/dossiers/show/status_overview.html.haml', dossier: dossier }
matcher :have_timeline_item do |selector| matcher :have_timeline_item do |selector|
@ -46,6 +48,7 @@ describe 'users/dossiers/show/_status_overview.html.haml', type: :view do
end end
it { is_expected.to have_selector('.status-explanation .en-construction') } 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 end
context 'when en instruction' do context 'when en instruction' do
@ -59,6 +62,7 @@ describe 'users/dossiers/show/_status_overview.html.haml', type: :view do
end end
it { is_expected.to have_selector('.status-explanation .en-instruction') } 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 end
context 'when accepté' do context 'when accepté' do

4405
yarn.lock

File diff suppressed because it is too large Load diff