Merge pull request #5049 from betagouv/dev
This commit is contained in:
commit
4d4f8927ce
27 changed files with 427 additions and 464 deletions
1
Gemfile
1
Gemfile
|
@ -28,6 +28,7 @@ gem 'dotenv-rails', require: 'dotenv/rails-now' # dotenv should always be loaded
|
||||||
gem 'flipper'
|
gem 'flipper'
|
||||||
gem 'flipper-active_record'
|
gem 'flipper-active_record'
|
||||||
gem 'flipper-ui'
|
gem 'flipper-ui'
|
||||||
|
gem 'font-awesome-rails'
|
||||||
gem 'fugit'
|
gem 'fugit'
|
||||||
gem 'geocoder'
|
gem 'geocoder'
|
||||||
gem 'gon'
|
gem 'gon'
|
||||||
|
|
|
@ -236,6 +236,8 @@ GEM
|
||||||
fog-core (~> 2.1)
|
fog-core (~> 2.1)
|
||||||
fog-json (>= 1.0)
|
fog-json (>= 1.0)
|
||||||
ipaddress (>= 0.8)
|
ipaddress (>= 0.8)
|
||||||
|
font-awesome-rails (4.7.0.5)
|
||||||
|
railties (>= 3.2, < 6.1)
|
||||||
formatador (0.2.5)
|
formatador (0.2.5)
|
||||||
fugit (1.3.3)
|
fugit (1.3.3)
|
||||||
et-orbi (~> 1.1, >= 1.1.8)
|
et-orbi (~> 1.1, >= 1.1.8)
|
||||||
|
@ -751,6 +753,7 @@ DEPENDENCIES
|
||||||
flipper
|
flipper
|
||||||
flipper-active_record
|
flipper-active_record
|
||||||
flipper-ui
|
flipper-ui
|
||||||
|
font-awesome-rails
|
||||||
fugit
|
fugit
|
||||||
geocoder
|
geocoder
|
||||||
gon
|
gon
|
||||||
|
|
|
@ -33,6 +33,7 @@
|
||||||
// = require attestation_template_edit
|
// = require attestation_template_edit
|
||||||
|
|
||||||
// = require_self
|
// = require_self
|
||||||
|
// = require font-awesome
|
||||||
// = require leaflet
|
// = require leaflet
|
||||||
// = require franceconnect
|
// = require franceconnect
|
||||||
// = require bootstrap-wysihtml5
|
// = require bootstrap-wysihtml5
|
||||||
|
|
|
@ -39,9 +39,10 @@ class Champs::CarteController < ApplicationController
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
selection_utilisateur = ApiCartoService.generate_selection_utilisateur(coordinates)
|
selections_utilisateur = legacy_selections_utilisateur_to_polygons(coordinates)
|
||||||
selection_utilisateur[:source] = GeoArea.sources.fetch(:selection_utilisateur)
|
geo_areas += selections_utilisateur.map do |selection_utilisateur|
|
||||||
geo_areas << selection_utilisateur
|
selection_utilisateur.merge(source: GeoArea.sources.fetch(:selection_utilisateur))
|
||||||
|
end
|
||||||
|
|
||||||
@champ.geo_areas = geo_areas.map do |geo_area|
|
@champ.geo_areas = geo_areas.map do |geo_area|
|
||||||
GeoArea.new(geo_area)
|
GeoArea.new(geo_area)
|
||||||
|
@ -58,4 +59,17 @@ class Champs::CarteController < ApplicationController
|
||||||
flash.alert = 'Les données cartographiques sont temporairement indisponibles. Réessayez dans un instant.'
|
flash.alert = 'Les données cartographiques sont temporairement indisponibles. Réessayez dans un instant.'
|
||||||
response.status = 503
|
response.status = 503
|
||||||
end
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def legacy_selections_utilisateur_to_polygons(coordinates)
|
||||||
|
coordinates.map do |lat_longs|
|
||||||
|
{
|
||||||
|
geometry: {
|
||||||
|
type: 'Polygon',
|
||||||
|
coordinates: [lat_longs.map { |lat_long| [lat_long['lng'], lat_long['lat']] }]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -37,4 +37,18 @@ module ChampHelper
|
||||||
champs_piece_justificative_url(object.id)
|
champs_piece_justificative_url(object.id)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def geo_area_label(geo_area)
|
||||||
|
case geo_area.source
|
||||||
|
when GeoArea.sources.fetch(:cadastre)
|
||||||
|
capture do
|
||||||
|
concat "Parcelle n° #{geo_area.numero} - Feuille #{geo_area.code_arr} #{geo_area.section} #{geo_area.feuille} - #{geo_area.surface_parcelle.round} m"
|
||||||
|
concat content_tag(:sup, "2")
|
||||||
|
end
|
||||||
|
when GeoArea.sources.fetch(:quartier_prioritaire)
|
||||||
|
"#{geo_area.commune} : #{geo_area.nom}"
|
||||||
|
when GeoArea.sources.fetch(:parcelle_agricole)
|
||||||
|
"Culture : #{geo_area.culture} - Surface : #{geo_area.surface} ha"
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,48 +1,36 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import ReactMapboxGl, { ZoomControl, GeoJSONLayer } from 'react-mapbox-gl';
|
import ReactMapboxGl, { ZoomControl, GeoJSONLayer } from 'react-mapbox-gl';
|
||||||
import mapboxgl, { LngLatBounds } from 'mapbox-gl';
|
import mapboxgl from 'mapbox-gl';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
const Map = ReactMapboxGl({});
|
const Map = ReactMapboxGl({});
|
||||||
|
|
||||||
const MapReader = ({ geoData }) => {
|
const MapReader = ({ featureCollection }) => {
|
||||||
let [selectionCollection, cadastresCollection] = [[], []];
|
const [a1, a2, b1, b2] = featureCollection.bbox;
|
||||||
|
const boundData = [
|
||||||
|
[a1, a2],
|
||||||
|
[b1, b2]
|
||||||
|
];
|
||||||
|
|
||||||
for (let selection of geoData.selection.coordinates) {
|
const selectionsFeatureCollection = {
|
||||||
selectionCollection.push({
|
|
||||||
type: 'Feature',
|
|
||||||
geometry: {
|
|
||||||
type: 'Polygon',
|
|
||||||
coordinates: selection
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let cadastre of geoData.cadastres) {
|
|
||||||
cadastresCollection.push({
|
|
||||||
type: 'Feature',
|
|
||||||
geometry: {
|
|
||||||
type: 'Polygon',
|
|
||||||
coordinates: cadastre.geometry.coordinates[0]
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const selectionData = {
|
|
||||||
type: 'geojson',
|
|
||||||
data: {
|
|
||||||
type: 'FeatureCollection',
|
type: 'FeatureCollection',
|
||||||
features: selectionCollection
|
features: []
|
||||||
}
|
};
|
||||||
|
const cadastresFeatureCollection = {
|
||||||
|
type: 'FeatureCollection',
|
||||||
|
features: []
|
||||||
};
|
};
|
||||||
|
|
||||||
const cadastresData = {
|
for (let feature of featureCollection.features) {
|
||||||
type: 'geojson',
|
switch (feature.properties.source) {
|
||||||
data: {
|
case 'selection_utilisateur':
|
||||||
type: 'FeatureCollection',
|
selectionsFeatureCollection.features.push(feature);
|
||||||
features: cadastresCollection
|
break;
|
||||||
|
case 'cadastre':
|
||||||
|
cadastresFeatureCollection.features.push(feature);
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
const polygonSelectionFill = {
|
const polygonSelectionFill = {
|
||||||
'fill-color': '#EC3323',
|
'fill-color': '#EC3323',
|
||||||
|
@ -65,19 +53,6 @@ const MapReader = ({ geoData }) => {
|
||||||
'line-dasharray': [1, 1]
|
'line-dasharray': [1, 1]
|
||||||
};
|
};
|
||||||
|
|
||||||
let bounds = new LngLatBounds();
|
|
||||||
|
|
||||||
for (let selection of selectionCollection) {
|
|
||||||
for (let coordinate of selection.geometry.coordinates[0]) {
|
|
||||||
bounds.extend(coordinate);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let [swCoords, neCoords] = [
|
|
||||||
Object.values(bounds._sw),
|
|
||||||
Object.values(bounds._ne)
|
|
||||||
];
|
|
||||||
const boundData = [swCoords, neCoords];
|
|
||||||
|
|
||||||
if (!mapboxgl.supported()) {
|
if (!mapboxgl.supported()) {
|
||||||
return (
|
return (
|
||||||
<p>
|
<p>
|
||||||
|
@ -99,12 +74,12 @@ const MapReader = ({ geoData }) => {
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<GeoJSONLayer
|
<GeoJSONLayer
|
||||||
data={selectionData.data}
|
data={selectionsFeatureCollection}
|
||||||
fillPaint={polygonSelectionFill}
|
fillPaint={polygonSelectionFill}
|
||||||
linePaint={polygonSelectionLine}
|
linePaint={polygonSelectionLine}
|
||||||
/>
|
/>
|
||||||
<GeoJSONLayer
|
<GeoJSONLayer
|
||||||
data={cadastresData.data}
|
data={cadastresFeatureCollection}
|
||||||
fillPaint={polygonCadastresFill}
|
fillPaint={polygonCadastresFill}
|
||||||
linePaint={polygonCadastresLine}
|
linePaint={polygonCadastresLine}
|
||||||
/>
|
/>
|
||||||
|
@ -114,10 +89,10 @@ const MapReader = ({ geoData }) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
MapReader.propTypes = {
|
MapReader.propTypes = {
|
||||||
geoData: PropTypes.shape({
|
featureCollection: PropTypes.shape({
|
||||||
position: PropTypes.object,
|
type: PropTypes.string,
|
||||||
selection: PropTypes.object,
|
bbox: PropTypes.array,
|
||||||
cadastres: PropTypes.array
|
features: PropTypes.array
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -16,10 +16,6 @@ async function loadAndDrawMap(element) {
|
||||||
const { drawEditableMap } = await import('../../shared/carte-editor');
|
const { drawEditableMap } = await import('../../shared/carte-editor');
|
||||||
|
|
||||||
drawEditableMap(element, data);
|
drawEditableMap(element, data);
|
||||||
} else {
|
|
||||||
const { drawMap } = await import('../../shared/carte');
|
|
||||||
|
|
||||||
drawMap(element, data);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import Uploader from '../../shared/activestorage/uploader';
|
import Uploader from '../../shared/activestorage/uploader';
|
||||||
import ProgressBar from '../../shared/activestorage/progress-bar';
|
import { show, hide, toggle } from '@utils';
|
||||||
import { ajax, show, hide, toggle } from '@utils';
|
import { FAILURE_CONNECTIVITY } from '../../shared/activestorage/file-upload-error';
|
||||||
|
|
||||||
// Given a file input in a champ with a selected file, upload a file,
|
// Given a file input in a champ with a selected file, upload a file,
|
||||||
// then attach it to the dossier.
|
// then attach it to the dossier.
|
||||||
|
@ -11,27 +11,21 @@ export default class AutoUploadController {
|
||||||
constructor(input, file) {
|
constructor(input, file) {
|
||||||
this.input = input;
|
this.input = input;
|
||||||
this.file = file;
|
this.file = file;
|
||||||
|
this.uploader = new Uploader(
|
||||||
|
input,
|
||||||
|
file,
|
||||||
|
input.dataset.directUploadUrl,
|
||||||
|
input.dataset.autoAttachUrl
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Create, upload and attach the file.
|
||||||
|
// On failure, display an error message and throw a FileUploadError.
|
||||||
async start() {
|
async start() {
|
||||||
try {
|
try {
|
||||||
this._begin();
|
this._begin();
|
||||||
|
await this.uploader.start();
|
||||||
// Sanity checks
|
this._succeeded();
|
||||||
const autoAttachUrl = this.input.dataset.autoAttachUrl;
|
|
||||||
if (!autoAttachUrl) {
|
|
||||||
throw new Error('L’attribut "data-auto-attach-url" est manquant');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Upload the file (using Direct Upload)
|
|
||||||
let blobSignedId = await this._upload();
|
|
||||||
|
|
||||||
// Attach the blob to the champ
|
|
||||||
// (The request responds with Javascript, which displays the attachment HTML fragment).
|
|
||||||
await this._attach(blobSignedId, autoAttachUrl);
|
|
||||||
|
|
||||||
// Everything good: clear the original file input value
|
|
||||||
this.input.value = null;
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this._failed(error);
|
this._failed(error);
|
||||||
throw error;
|
throw error;
|
||||||
|
@ -45,35 +39,8 @@ export default class AutoUploadController {
|
||||||
this._hideErrorMessage();
|
this._hideErrorMessage();
|
||||||
}
|
}
|
||||||
|
|
||||||
async _upload() {
|
_succeeded() {
|
||||||
const uploader = new Uploader(
|
this.input.value = null;
|
||||||
this.input,
|
|
||||||
this.file,
|
|
||||||
this.input.dataset.directUploadUrl
|
|
||||||
);
|
|
||||||
return await uploader.start();
|
|
||||||
}
|
|
||||||
|
|
||||||
async _attach(blobSignedId, autoAttachUrl) {
|
|
||||||
// Now that the upload is done, display a new progress bar
|
|
||||||
// to show that the attachment request is still pending.
|
|
||||||
const progressBar = new ProgressBar(
|
|
||||||
this.input,
|
|
||||||
`${this.input.id}-progress-bar`,
|
|
||||||
this.file
|
|
||||||
);
|
|
||||||
progressBar.progress(100);
|
|
||||||
progressBar.end();
|
|
||||||
|
|
||||||
const attachmentRequest = {
|
|
||||||
url: autoAttachUrl,
|
|
||||||
type: 'PUT',
|
|
||||||
data: `blob_signed_id=${blobSignedId}`
|
|
||||||
};
|
|
||||||
await ajax(attachmentRequest);
|
|
||||||
|
|
||||||
// The progress bar has been destroyed by the attachment HTML fragment that replaced the input,
|
|
||||||
// so no further cleanup is needed.
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_failed(error) {
|
_failed(error) {
|
||||||
|
@ -81,56 +48,39 @@ export default class AutoUploadController {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let progressBar = this.input.parentElement.querySelector('.direct-upload');
|
this.uploader.progressBar.destroy();
|
||||||
if (progressBar) {
|
|
||||||
progressBar.remove();
|
|
||||||
}
|
|
||||||
|
|
||||||
this._displayErrorMessage(error);
|
let message = this._messageFromError(error);
|
||||||
|
this._displayErrorMessage(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
_done() {
|
_done() {
|
||||||
this.input.disabled = false;
|
this.input.disabled = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
_isError422(error) {
|
|
||||||
// Ajax errors have an xhr attribute
|
|
||||||
if (error && error.xhr && error.xhr.status == 422) return true;
|
|
||||||
// Rails DirectUpload errors are returned as a String, e.g. 'Error creating Blob for "Demain.txt". Status: 422'
|
|
||||||
if (error && error.toString().includes('422')) return true;
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
_messageFromError(error) {
|
_messageFromError(error) {
|
||||||
let allowRetry = !this._isError422(error);
|
let message = error.message || error.toString();
|
||||||
|
let canRetry = error.status && error.status != 422;
|
||||||
|
|
||||||
if (
|
if (error.failureReason == FAILURE_CONNECTIVITY) {
|
||||||
error.xhr &&
|
|
||||||
error.xhr.status == 422 &&
|
|
||||||
error.response &&
|
|
||||||
error.response.errors &&
|
|
||||||
error.response.errors[0]
|
|
||||||
) {
|
|
||||||
return {
|
return {
|
||||||
title: error.response.errors[0],
|
title: 'Le fichier n’a pas pu être envoyé.',
|
||||||
description: '',
|
description: 'Vérifiez votre connexion à Internet, puis ré-essayez.',
|
||||||
retry: allowRetry
|
retry: true
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
return {
|
return {
|
||||||
title: 'Une erreur s’est produite pendant l’envoi du fichier.',
|
title: 'Le fichier n’a pas pu être envoyé.',
|
||||||
description: error.message || error.toString(),
|
description: message,
|
||||||
retry: allowRetry
|
retry: canRetry
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_displayErrorMessage(error) {
|
_displayErrorMessage(message) {
|
||||||
let errorNode = this.input.parentElement.querySelector('.attachment-error');
|
let errorNode = this.input.parentElement.querySelector('.attachment-error');
|
||||||
if (errorNode) {
|
if (errorNode) {
|
||||||
show(errorNode);
|
show(errorNode);
|
||||||
let message = this._messageFromError(error);
|
|
||||||
errorNode.querySelector('.attachment-error-title').textContent =
|
errorNode.querySelector('.attachment-error-title').textContent =
|
||||||
message.title || '';
|
message.title || '';
|
||||||
errorNode.querySelector('.attachment-error-description').textContent =
|
errorNode.querySelector('.attachment-error-description').textContent =
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import Rails from '@rails/ujs';
|
import Rails from '@rails/ujs';
|
||||||
import AutoUploadController from './auto-upload-controller.js';
|
import AutoUploadController from './auto-upload-controller.js';
|
||||||
|
import { FAILURE_CONNECTIVITY } from '../../shared/activestorage/file-upload-error';
|
||||||
|
|
||||||
// Manage multiple concurrent uploads.
|
// Manage multiple concurrent uploads.
|
||||||
//
|
//
|
||||||
|
@ -17,6 +18,11 @@ export default class AutoUploadsControllers {
|
||||||
try {
|
try {
|
||||||
let controller = new AutoUploadController(input, file);
|
let controller = new AutoUploadController(input, file);
|
||||||
await controller.start();
|
await controller.start();
|
||||||
|
} catch (error) {
|
||||||
|
// Report errors to Sentry (except connectivity issues)
|
||||||
|
if (error.failureReason != FAILURE_CONNECTIVITY) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
this._decrementInFlightUploads(form);
|
this._decrementInFlightUploads(form);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,22 +0,0 @@
|
||||||
// Convert an error message returned by DirectUpload to a proper error object.
|
|
||||||
//
|
|
||||||
// This function has two goals:
|
|
||||||
// 1. Remove the file name from the DirectUpload error message
|
|
||||||
// (because the filename confuses Sentry error grouping)
|
|
||||||
// 2. Create each kind of error on a different line
|
|
||||||
// (so that Sentry knows they are different kind of errors, from
|
|
||||||
// the line they were created.)
|
|
||||||
export default function errorFromDirectUploadMessage(message) {
|
|
||||||
let matches = message.match(/ Status: [0-9]{1,3}/);
|
|
||||||
let status = (matches && matches[0]) || '';
|
|
||||||
|
|
||||||
if (message.includes('Error creating')) {
|
|
||||||
return new Error('Error creating file.' + status);
|
|
||||||
} else if (message.includes('Error storing')) {
|
|
||||||
return new Error('Error storing file.' + status);
|
|
||||||
} else if (message.includes('Error reading')) {
|
|
||||||
return new Error('Error reading file.' + status);
|
|
||||||
} else {
|
|
||||||
return new Error(message);
|
|
||||||
}
|
|
||||||
}
|
|
67
app/javascript/shared/activestorage/file-upload-error.js
Normal file
67
app/javascript/shared/activestorage/file-upload-error.js
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
// Error while reading the file client-side
|
||||||
|
export const ERROR_CODE_READ = 'file-upload-read-error';
|
||||||
|
// Error while creating the empty blob on the server
|
||||||
|
export const ERROR_CODE_CREATE = 'file-upload-create-error';
|
||||||
|
// Error while uploading the blob content
|
||||||
|
export const ERROR_CODE_STORE = 'file-upload-store-error';
|
||||||
|
// Error while attaching the blob to the record
|
||||||
|
export const ERROR_CODE_ATTACH = 'file-upload-attach-error';
|
||||||
|
|
||||||
|
// Failure on the client side (syntax error, error reading file, etc.)
|
||||||
|
export const FAILURE_CLIENT = 'file-upload-failure-client';
|
||||||
|
// Failure on the server side (typically non-200 response)
|
||||||
|
export const FAILURE_SERVER = 'file-upload-failure-server';
|
||||||
|
// Failure during the transfert (request aborted, connection lost, etc)
|
||||||
|
export const FAILURE_CONNECTIVITY = 'file-upload-failure-connectivity';
|
||||||
|
|
||||||
|
/**
|
||||||
|
Represent an error during a file upload.
|
||||||
|
*/
|
||||||
|
export default class FileUploadError extends Error {
|
||||||
|
constructor(message, status, code) {
|
||||||
|
super(message);
|
||||||
|
this.name = 'FileUploadError';
|
||||||
|
this.status = status;
|
||||||
|
this.code = code;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
Return the component responsible of the error (client, server or connectivity).
|
||||||
|
See FAILURE_* constants for values.
|
||||||
|
*/
|
||||||
|
get failureReason() {
|
||||||
|
let isNetworkError = this.code != ERROR_CODE_READ;
|
||||||
|
|
||||||
|
if (isNetworkError && this.status != 0) {
|
||||||
|
return FAILURE_SERVER;
|
||||||
|
} else if (isNetworkError && this.status == 0) {
|
||||||
|
return FAILURE_CONNECTIVITY;
|
||||||
|
} else {
|
||||||
|
return FAILURE_CLIENT;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert an error message returned by DirectUpload to a proper error object.
|
||||||
|
//
|
||||||
|
// This function has two goals:
|
||||||
|
// 1. Remove the file name from the DirectUpload error message
|
||||||
|
// (because the filename confuses Sentry error grouping)
|
||||||
|
// 2. Create each kind of error on a different line
|
||||||
|
// (so that Sentry knows they are different kind of errors, from
|
||||||
|
// the line they were created.)
|
||||||
|
export function errorFromDirectUploadMessage(message) {
|
||||||
|
let matches = message.match(/ Status: [0-9]{1,3}/);
|
||||||
|
let status = (matches && parseInt(matches[0], 10)) || undefined;
|
||||||
|
|
||||||
|
// prettier-ignore
|
||||||
|
if (message.includes('Error reading')) {
|
||||||
|
return new FileUploadError('Error reading file.', status, ERROR_CODE_READ);
|
||||||
|
} else if (message.includes('Error creating')) {
|
||||||
|
return new FileUploadError('Error creating file.', status, ERROR_CODE_CREATE);
|
||||||
|
} else if (message.includes('Error storing')) {
|
||||||
|
return new FileUploadError('Error storing file.', status, ERROR_CODE_STORE);
|
||||||
|
} else {
|
||||||
|
return new FileUploadError(message, status, undefined);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,5 +1,8 @@
|
||||||
import ProgressBar from './progress-bar';
|
import ProgressBar from './progress-bar';
|
||||||
import errorFromDirectUploadMessage from './errors';
|
import {
|
||||||
|
errorFromDirectUploadMessage,
|
||||||
|
FAILURE_CONNECTIVITY
|
||||||
|
} from './file-upload-error';
|
||||||
import { fire } from '@utils';
|
import { fire } from '@utils';
|
||||||
|
|
||||||
const INITIALIZE_EVENT = 'direct-upload:initialize';
|
const INITIALIZE_EVENT = 'direct-upload:initialize';
|
||||||
|
@ -56,7 +59,9 @@ addUploadEventListener(ERROR_EVENT, event => {
|
||||||
ProgressBar.error(id, errorMsg);
|
ProgressBar.error(id, errorMsg);
|
||||||
|
|
||||||
let error = errorFromDirectUploadMessage(errorMsg);
|
let error = errorFromDirectUploadMessage(errorMsg);
|
||||||
|
if (error.failureReason != FAILURE_CONNECTIVITY) {
|
||||||
fire(document, 'sentry:capture-exception', error);
|
fire(document, 'sentry:capture-exception', error);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
addUploadEventListener(END_EVENT, ({ detail: { id } }) => {
|
addUploadEventListener(END_EVENT, ({ detail: { id } }) => {
|
||||||
|
|
|
@ -1,35 +1,88 @@
|
||||||
import { DirectUpload } from '@rails/activestorage';
|
import { DirectUpload } from '@rails/activestorage';
|
||||||
|
import { ajax } from '@utils';
|
||||||
import ProgressBar from './progress-bar';
|
import ProgressBar from './progress-bar';
|
||||||
import errorFromDirectUploadMessage from './errors';
|
import FileUploadError, {
|
||||||
|
errorFromDirectUploadMessage,
|
||||||
|
ERROR_CODE_ATTACH
|
||||||
|
} from './file-upload-error';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Uploader class is a delegate for DirectUpload instance
|
Uploader class is a delegate for DirectUpload instance
|
||||||
used to track lifecycle and progress of an upload.
|
used to track lifecycle and progress of an upload.
|
||||||
*/
|
*/
|
||||||
export default class Uploader {
|
export default class Uploader {
|
||||||
constructor(input, file, directUploadUrl) {
|
constructor(input, file, directUploadUrl, autoAttachUrl) {
|
||||||
this.directUpload = new DirectUpload(file, directUploadUrl, this);
|
this.directUpload = new DirectUpload(file, directUploadUrl, this);
|
||||||
this.progressBar = new ProgressBar(input, this.directUpload.id, file);
|
this.progressBar = new ProgressBar(input, this.directUpload.id, file);
|
||||||
|
this.autoAttachUrl = autoAttachUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
start() {
|
/**
|
||||||
|
Upload (and optionally attach) the file.
|
||||||
|
Returns the blob signed id on success.
|
||||||
|
Throws a FileUploadError on failure.
|
||||||
|
*/
|
||||||
|
async start() {
|
||||||
this.progressBar.start();
|
this.progressBar.start();
|
||||||
|
|
||||||
|
try {
|
||||||
|
let blobSignedId = await this._upload();
|
||||||
|
|
||||||
|
if (this.autoAttachUrl) {
|
||||||
|
await this._attach(blobSignedId);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.progressBar.end();
|
||||||
|
this.progressBar.destroy();
|
||||||
|
|
||||||
|
return blobSignedId;
|
||||||
|
} catch (error) {
|
||||||
|
this.progressBar.error(error.message);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
Upload the file using the DirectUpload instance, and return the blob signed_id.
|
||||||
|
Throws a FileUploadError on failure.
|
||||||
|
*/
|
||||||
|
async _upload() {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
this.directUpload.create((errorMsg, attributes) => {
|
this.directUpload.create((errorMsg, attributes) => {
|
||||||
if (errorMsg) {
|
if (errorMsg) {
|
||||||
this.progressBar.error(errorMsg);
|
|
||||||
let error = errorFromDirectUploadMessage(errorMsg);
|
let error = errorFromDirectUploadMessage(errorMsg);
|
||||||
reject(error);
|
reject(error);
|
||||||
} else {
|
} else {
|
||||||
resolve(attributes.signed_id);
|
resolve(attributes.signed_id);
|
||||||
}
|
}
|
||||||
this.progressBar.end();
|
|
||||||
this.progressBar.destroy();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
Attach the file by sending a POST request to the autoAttachUrl.
|
||||||
|
Throws a FileUploadError on failure (containing the first validation
|
||||||
|
error message, if any).
|
||||||
|
*/
|
||||||
|
async _attach(blobSignedId) {
|
||||||
|
const attachmentRequest = {
|
||||||
|
url: this.autoAttachUrl,
|
||||||
|
type: 'PUT',
|
||||||
|
data: `blob_signed_id=${blobSignedId}`
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
await ajax(attachmentRequest);
|
||||||
|
} catch (e) {
|
||||||
|
let message = e.response && e.response.errors && e.response.errors[0];
|
||||||
|
throw new FileUploadError(
|
||||||
|
message || 'Error attaching file.',
|
||||||
|
e.xhr.status,
|
||||||
|
ERROR_CODE_ATTACH
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
uploadRequestDidProgress(event) {
|
uploadRequestDidProgress(event) {
|
||||||
const progress = (event.loaded / event.total) * 100;
|
const progress = (event.loaded / event.total) * 100;
|
||||||
if (progress) {
|
if (progress) {
|
||||||
|
|
|
@ -1,104 +0,0 @@
|
||||||
import L from 'leaflet';
|
|
||||||
|
|
||||||
const MAPS = new WeakMap();
|
|
||||||
|
|
||||||
export function drawMap(element, data) {
|
|
||||||
const map = initMap(element, data);
|
|
||||||
|
|
||||||
drawCadastre(map, data);
|
|
||||||
drawQuartiersPrioritaires(map, data);
|
|
||||||
drawParcellesAgricoles(map, data);
|
|
||||||
drawUserSelection(map, data);
|
|
||||||
}
|
|
||||||
|
|
||||||
function initMap(element, { position }) {
|
|
||||||
if (MAPS.has(element)) {
|
|
||||||
return MAPS.get(element);
|
|
||||||
} else {
|
|
||||||
const map = L.map(element, {
|
|
||||||
scrollWheelZoom: false
|
|
||||||
}).setView([position.lat, position.lon], position.zoom);
|
|
||||||
|
|
||||||
const loadTilesLayer = process.env.RAILS_ENV != 'test';
|
|
||||||
if (loadTilesLayer) {
|
|
||||||
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
|
||||||
attribution:
|
|
||||||
'© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
|
|
||||||
}).addTo(map);
|
|
||||||
}
|
|
||||||
|
|
||||||
MAPS.set(element, map);
|
|
||||||
return map;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function drawUserSelection(map, { selection }) {
|
|
||||||
if (selection) {
|
|
||||||
const layer = L.geoJSON(selection, {
|
|
||||||
style: USER_SELECTION_POLYGON_STYLE
|
|
||||||
});
|
|
||||||
|
|
||||||
layer.addTo(map);
|
|
||||||
|
|
||||||
map.fitBounds(layer.getBounds());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function drawCadastre(map, { cadastres }) {
|
|
||||||
drawLayer(map, cadastres, noEditStyle(CADASTRE_POLYGON_STYLE));
|
|
||||||
}
|
|
||||||
|
|
||||||
function drawQuartiersPrioritaires(map, { quartiersPrioritaires }) {
|
|
||||||
drawLayer(map, quartiersPrioritaires, noEditStyle(QP_POLYGON_STYLE));
|
|
||||||
}
|
|
||||||
|
|
||||||
function drawParcellesAgricoles(map, { parcellesAgricoles }) {
|
|
||||||
drawLayer(map, parcellesAgricoles, noEditStyle(RPG_POLYGON_STYLE));
|
|
||||||
}
|
|
||||||
|
|
||||||
function drawLayer(map, data, style) {
|
|
||||||
if (Array.isArray(data) && data.length > 0) {
|
|
||||||
const layer = new L.GeoJSON(undefined, {
|
|
||||||
interactive: false,
|
|
||||||
style
|
|
||||||
});
|
|
||||||
|
|
||||||
for (let { geometry } of data) {
|
|
||||||
layer.addData(geometry);
|
|
||||||
}
|
|
||||||
|
|
||||||
layer.addTo(map);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function noEditStyle(style) {
|
|
||||||
return Object.assign({}, style, {
|
|
||||||
opacity: 0.7,
|
|
||||||
fillOpacity: 0.5,
|
|
||||||
color: style.fillColor
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const POLYGON_STYLE = {
|
|
||||||
weight: 2,
|
|
||||||
opacity: 0.3,
|
|
||||||
color: 'white',
|
|
||||||
dashArray: '3',
|
|
||||||
fillOpacity: 0.7
|
|
||||||
};
|
|
||||||
|
|
||||||
const CADASTRE_POLYGON_STYLE = Object.assign({}, POLYGON_STYLE, {
|
|
||||||
fillColor: '#8a6d3b'
|
|
||||||
});
|
|
||||||
|
|
||||||
const QP_POLYGON_STYLE = Object.assign({}, POLYGON_STYLE, {
|
|
||||||
fillColor: '#31708f'
|
|
||||||
});
|
|
||||||
|
|
||||||
const RPG_POLYGON_STYLE = Object.assign({}, POLYGON_STYLE, {
|
|
||||||
fillColor: '#31708f'
|
|
||||||
});
|
|
||||||
|
|
||||||
const USER_SELECTION_POLYGON_STYLE = {
|
|
||||||
color: 'red'
|
|
||||||
};
|
|
|
@ -1,4 +1,8 @@
|
||||||
class Champs::CarteChamp < Champ
|
class Champs::CarteChamp < Champ
|
||||||
|
# Default map location. Center of the World, ahm, France...
|
||||||
|
DEFAULT_LON = 2.428462
|
||||||
|
DEFAULT_LAT = 46.538192
|
||||||
|
|
||||||
# We are not using scopes here as we want to access
|
# We are not using scopes here as we want to access
|
||||||
# the following collections on unsaved records.
|
# the following collections on unsaved records.
|
||||||
def cadastres
|
def cadastres
|
||||||
|
@ -19,8 +23,10 @@ class Champs::CarteChamp < Champ
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def selection_utilisateur
|
def selections_utilisateur
|
||||||
geo_areas.find(&:selection_utilisateur?)
|
geo_areas.filter do |area|
|
||||||
|
area.source == GeoArea.sources.fetch(:selection_utilisateur)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def cadastres?
|
def cadastres?
|
||||||
|
@ -39,76 +45,81 @@ class Champs::CarteChamp < Champ
|
||||||
if dossier.present?
|
if dossier.present?
|
||||||
dossier.geo_position
|
dossier.geo_position
|
||||||
else
|
else
|
||||||
lon = "2.428462"
|
lon = DEFAULT_LON.to_s
|
||||||
lat = "46.538192"
|
lat = DEFAULT_LAT.to_s
|
||||||
zoom = "13"
|
zoom = "13"
|
||||||
|
|
||||||
{ lon: lon, lat: lat, zoom: zoom }
|
{ lon: lon, lat: lat, zoom: zoom }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def geo_json
|
def bounding_box
|
||||||
@geo_json ||= begin
|
factory = RGeo::Geographic.simple_mercator_factory
|
||||||
geo_area = selection_utilisateur
|
bounding_box = RGeo::Cartesian::BoundingBox.new(factory)
|
||||||
|
|
||||||
if geo_area
|
if geo_areas.present?
|
||||||
geo_area.geometry
|
geo_areas.each do |area|
|
||||||
else
|
bounding_box.add(area.rgeo_geometry)
|
||||||
geo_json_from_value
|
|
||||||
end
|
end
|
||||||
|
elsif dossier.present?
|
||||||
|
point = dossier.geo_position
|
||||||
|
bounding_box.add(factory.point(point[:lon], point[:lat]))
|
||||||
|
else
|
||||||
|
bounding_box.add(factory.point(DEFAULT_LON, DEFAULT_LAT))
|
||||||
|
end
|
||||||
|
|
||||||
|
[bounding_box.max_point, bounding_box.min_point].compact.flat_map(&:coordinates)
|
||||||
|
end
|
||||||
|
|
||||||
|
def to_feature_collection
|
||||||
|
{
|
||||||
|
type: 'FeatureCollection',
|
||||||
|
bbox: bounding_box,
|
||||||
|
features: (legacy_selections_utilisateur + except_selections_utilisateur).map(&:to_feature)
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def geometry?
|
||||||
|
geo_areas.present?
|
||||||
|
end
|
||||||
|
|
||||||
|
def selection_utilisateur_legacy_geometry
|
||||||
|
if selection_utilisateur_legacy?
|
||||||
|
selections_utilisateur.first.geometry
|
||||||
|
elsif selections_utilisateur.present?
|
||||||
|
{
|
||||||
|
type: 'MultiPolygon',
|
||||||
|
coordinates: selections_utilisateur.filter do |selection_utilisateur|
|
||||||
|
selection_utilisateur.geometry['type'] == 'Polygon'
|
||||||
|
end.map do |selection_utilisateur|
|
||||||
|
selection_utilisateur.geometry['coordinates']
|
||||||
|
end
|
||||||
|
}
|
||||||
|
else
|
||||||
|
nil
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def selection_utilisateur_size
|
def selection_utilisateur_legacy_geo_area
|
||||||
if geo_json.present?
|
geometry = selection_utilisateur_legacy_geometry
|
||||||
geo_json['coordinates'].size
|
if geometry.present?
|
||||||
else
|
GeoArea.new(
|
||||||
0
|
source: GeoArea.sources.fetch(:selection_utilisateur),
|
||||||
|
geometry: geometry
|
||||||
|
)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def to_render_data
|
def to_render_data
|
||||||
{
|
{
|
||||||
position: position,
|
position: position,
|
||||||
selection: user_geo_area&.geometry,
|
selection: selection_utilisateur_legacy_geometry,
|
||||||
quartiersPrioritaires: quartiers_prioritaires? ? quartiers_prioritaires.as_json(except: :properties) : [],
|
quartiersPrioritaires: quartiers_prioritaires? ? quartiers_prioritaires.as_json(except: :properties) : [],
|
||||||
cadastres: cadastres? ? cadastres.as_json(except: :properties) : [],
|
cadastres: cadastres? ? cadastres.as_json(except: :properties) : [],
|
||||||
parcellesAgricoles: parcelles_agricoles? ? parcelles_agricoles.as_json(except: :properties) : []
|
parcellesAgricoles: parcelles_agricoles? ? parcelles_agricoles.as_json(except: :properties) : []
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
def user_geo_area
|
|
||||||
geo_area = selection_utilisateur
|
|
||||||
|
|
||||||
if geo_area.present?
|
|
||||||
geo_area
|
|
||||||
elsif geo_json_from_value.present?
|
|
||||||
GeoArea.new(
|
|
||||||
geometry: geo_json_from_value,
|
|
||||||
source: GeoArea.sources.fetch(:selection_utilisateur)
|
|
||||||
)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def geo_json_from_value
|
|
||||||
@geo_json_from_value ||= begin
|
|
||||||
parsed_value = value.blank? ? nil : JSON.parse(value)
|
|
||||||
# We used to store in the value column a json array with coordinates.
|
|
||||||
if parsed_value.is_a?(Array)
|
|
||||||
# Empty array is sent instead of blank to distinguish between empty and error
|
|
||||||
if parsed_value.empty?
|
|
||||||
nil
|
|
||||||
else
|
|
||||||
# If it is a coordinates array, format it as a GEO-JSON
|
|
||||||
JSON.parse(GeojsonService.to_json_polygon_for_selection_utilisateur(parsed_value))
|
|
||||||
end
|
|
||||||
else
|
|
||||||
# It is already a GEO-JSON
|
|
||||||
parsed_value
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def for_api
|
def for_api
|
||||||
nil
|
nil
|
||||||
end
|
end
|
||||||
|
@ -116,4 +127,38 @@ class Champs::CarteChamp < Champ
|
||||||
def for_export
|
def for_export
|
||||||
nil
|
nil
|
||||||
end
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def selection_utilisateur_legacy?
|
||||||
|
if selections_utilisateur.size == 1
|
||||||
|
geometry = selections_utilisateur.first.geometry
|
||||||
|
return geometry && geometry['type'] == 'MultiPolygon'
|
||||||
|
end
|
||||||
|
|
||||||
|
false
|
||||||
|
end
|
||||||
|
|
||||||
|
def legacy_selections_utilisateur
|
||||||
|
if selection_utilisateur_legacy?
|
||||||
|
selections_utilisateur.first.geometry['coordinates'].map do |coordinates|
|
||||||
|
GeoArea.new(
|
||||||
|
geometry: {
|
||||||
|
type: 'Polygon',
|
||||||
|
coordinates: coordinates
|
||||||
|
},
|
||||||
|
properties: {},
|
||||||
|
source: GeoArea.sources.fetch(:selection_utilisateur)
|
||||||
|
)
|
||||||
|
end
|
||||||
|
else
|
||||||
|
selections_utilisateur
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def except_selections_utilisateur
|
||||||
|
geo_areas.filter do |area|
|
||||||
|
area.source != GeoArea.sources.fetch(:selection_utilisateur)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -425,8 +425,8 @@ class Dossier < ApplicationRecord
|
||||||
point = Geocoder.search(etablissement.geo_adresse).first
|
point = Geocoder.search(etablissement.geo_adresse).first
|
||||||
end
|
end
|
||||||
|
|
||||||
lon = "2.428462"
|
lon = Champs::CarteChamp::DEFAULT_LON.to_s
|
||||||
lat = "46.538192"
|
lat = Champs::CarteChamp::DEFAULT_LAT.to_s
|
||||||
zoom = "13"
|
zoom = "13"
|
||||||
|
|
||||||
if point.present?
|
if point.present?
|
||||||
|
|
|
@ -27,11 +27,20 @@ class GeoArea < ApplicationRecord
|
||||||
selection_utilisateur: 'selection_utilisateur'
|
selection_utilisateur: 'selection_utilisateur'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
scope :selections_utilisateur, -> { where(source: sources.fetch(:selection_utilisateur)) }
|
||||||
scope :quartiers_prioritaires, -> { where(source: sources.fetch(:quartier_prioritaire)) }
|
scope :quartiers_prioritaires, -> { where(source: sources.fetch(:quartier_prioritaire)) }
|
||||||
scope :cadastres, -> { where(source: sources.fetch(:cadastre)) }
|
scope :cadastres, -> { where(source: sources.fetch(:cadastre)) }
|
||||||
scope :parcelles_agricoles, -> { where(source: sources.fetch(:parcelle_agricole)) }
|
scope :parcelles_agricoles, -> { where(source: sources.fetch(:parcelle_agricole)) }
|
||||||
|
|
||||||
def selection_utilisateur?
|
def to_feature
|
||||||
source == self.class.sources.fetch(:selection_utilisateur)
|
{
|
||||||
|
type: 'Feature',
|
||||||
|
geometry: geometry,
|
||||||
|
properties: properties.merge(source: source)
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def rgeo_geometry
|
||||||
|
RGeo::GeoJSON.decode(geometry.to_json, geo_factory: RGeo::Geographic.simple_mercator_factory)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -38,11 +38,12 @@ class DossierSerializer < ActiveModel::Serializer
|
||||||
end
|
end
|
||||||
|
|
||||||
if champ_carte.present?
|
if champ_carte.present?
|
||||||
carto_champs = champ_carte.geo_areas.to_a
|
champs_geo_areas = geo_areas.filter do |geo_area|
|
||||||
if !carto_champs.find(&:selection_utilisateur?)
|
geo_area.source != GeoArea.sources.fetch(:selection_utilisateur)
|
||||||
carto_champs << champ_carte.user_geo_area
|
|
||||||
end
|
end
|
||||||
champs += carto_champs.compact
|
champs_geo_areas << champ_carte.selection_utilisateur_legacy_geo_area
|
||||||
|
|
||||||
|
champs += champs_geo_areas.compact
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -22,10 +22,4 @@ class ApiCartoService
|
||||||
).results
|
).results
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.generate_selection_utilisateur(coordinates)
|
|
||||||
{
|
|
||||||
geometry: JSON.parse(GeojsonService.to_json_polygon_for_selection_utilisateur(coordinates))
|
|
||||||
}
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
|
@ -14,21 +14,4 @@ class GeojsonService
|
||||||
|
|
||||||
polygon.to_json
|
polygon.to_json
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.to_json_polygon_for_selection_utilisateur(coordinates)
|
|
||||||
coordinates = coordinates.map do |lat_longs|
|
|
||||||
outbounds = lat_longs.map do |lat_long|
|
|
||||||
[lat_long['lng'], lat_long['lat']]
|
|
||||||
end
|
|
||||||
|
|
||||||
[outbounds]
|
|
||||||
end
|
|
||||||
|
|
||||||
polygon = {
|
|
||||||
type: 'MultiPolygon',
|
|
||||||
coordinates: coordinates
|
|
||||||
}
|
|
||||||
|
|
||||||
polygon.to_json
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
|
@ -86,7 +86,7 @@ def render_single_champ(pdf, champ)
|
||||||
when 'Champs::ExplicationChamp'
|
when 'Champs::ExplicationChamp'
|
||||||
format_in_2_lines(pdf, champ.libelle, champ.description)
|
format_in_2_lines(pdf, champ.libelle, champ.description)
|
||||||
when 'Champs::CarteChamp'
|
when 'Champs::CarteChamp'
|
||||||
format_in_2_lines(pdf, champ.libelle, champ.geo_json.to_s)
|
format_in_2_lines(pdf, champ.libelle, champ.to_feature_collection.to_json)
|
||||||
when 'Champs::SiretChamp'
|
when 'Champs::SiretChamp'
|
||||||
pdf.font 'liberation serif', style: :bold, size: 12 do
|
pdf.font 'liberation serif', style: :bold, size: 12 do
|
||||||
pdf.text champ.libelle
|
pdf.text champ.libelle
|
||||||
|
|
|
@ -3,39 +3,39 @@
|
||||||
.areas
|
.areas
|
||||||
- if error.present?
|
- if error.present?
|
||||||
.error Merci de dessiner une surface plus petite afin de récupérer les quartiers prioritaires.
|
.error Merci de dessiner une surface plus petite afin de récupérer les quartiers prioritaires.
|
||||||
- elsif champ.value.blank?
|
- elsif !champ.geometry?
|
||||||
Aucune zone tracée
|
Aucune zone tracée
|
||||||
- elsif champ.quartiers_prioritaires.blank?
|
- elsif champ.quartiers_prioritaires.blank?
|
||||||
= t('errors.messages.quartiers_prioritaires_empty', count: champ.selection_utilisateur_size)
|
= t('errors.messages.quartiers_prioritaires_empty', count: champ.selections_utilisateur.size)
|
||||||
- else
|
- else
|
||||||
%ul
|
%ul
|
||||||
- champ.quartiers_prioritaires.each do |qp|
|
- champ.quartiers_prioritaires.each do |geo_area|
|
||||||
%li #{qp.commune} : #{qp.nom}
|
%li= geo_area_label(geo_area)
|
||||||
|
|
||||||
- if champ.cadastres?
|
- if champ.cadastres?
|
||||||
.areas-title Parcelles cadastrales
|
.areas-title Parcelles cadastrales
|
||||||
.areas
|
.areas
|
||||||
- if error.present?
|
- if error.present?
|
||||||
.error Merci de dessiner une surface plus petite afin de récupérer les parcelles cadastrales.
|
.error Merci de dessiner une surface plus petite afin de récupérer les parcelles cadastrales.
|
||||||
- elsif champ.value.blank?
|
- elsif !champ.geometry?
|
||||||
Aucune zone tracée
|
Aucune zone tracée
|
||||||
- elsif champ.cadastres.blank?
|
- elsif champ.cadastres.blank?
|
||||||
= t('errors.messages.cadastres_empty', count: champ.selection_utilisateur_size)
|
= t('errors.messages.cadastres_empty', count: champ.selections_utilisateur.size)
|
||||||
- else
|
- else
|
||||||
%ul
|
%ul
|
||||||
- champ.cadastres.each do |pc|
|
- champ.cadastres.each do |geo_area|
|
||||||
%li Parcelle n° #{pc.numero} - Feuille #{pc.code_arr} #{pc.section} #{pc.feuille} - #{pc.surface_parcelle.round} m<sup>2</sup>
|
%li= geo_area_label(geo_area)
|
||||||
|
|
||||||
- if champ.parcelles_agricoles?
|
- if champ.parcelles_agricoles?
|
||||||
.areas-title Parcelles agricoles (RPG)
|
.areas-title Parcelles agricoles (RPG)
|
||||||
.areas
|
.areas
|
||||||
- if error.present?
|
- if error.present?
|
||||||
.error Merci de dessiner une surface plus petite afin de récupérer les parcelles agricoles.
|
.error Merci de dessiner une surface plus petite afin de récupérer les parcelles agricoles.
|
||||||
- elsif champ.value.blank?
|
- elsif !champ.geometry?
|
||||||
Aucune zone tracée
|
Aucune zone tracée
|
||||||
- elsif champ.parcelles_agricoles.blank?
|
- elsif champ.parcelles_agricoles.blank?
|
||||||
= t('errors.messages.parcelles_agricoles_empty', count: champ.selection_utilisateur_size)
|
= t('errors.messages.parcelles_agricoles_empty', count: champ.selections_utilisateur.size)
|
||||||
- else
|
- else
|
||||||
%ul
|
%ul
|
||||||
- champ.parcelles_agricoles.each do |pa|
|
- champ.parcelles_agricoles.each do |geo_area|
|
||||||
%li Culture : #{pa.culture} - Surface : #{pa.surface} ha
|
%li= geo_area_label(geo_area)
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
- if champ.to_s.present?
|
- if champ.geometry?
|
||||||
= react_component("MapReader", { geoData: champ.to_render_data } )
|
= react_component("MapReader", { featureCollection: champ.to_feature_collection } )
|
||||||
.geo-areas
|
.geo-areas
|
||||||
= render partial: 'shared/champs/carte/geo_areas', locals: { champ: champ, error: false }
|
= render partial: 'shared/champs/carte/geo_areas', locals: { champ: champ, error: false }
|
||||||
|
|
|
@ -0,0 +1,18 @@
|
||||||
|
namespace :after_party do
|
||||||
|
desc 'Deployment task: split_geo_area_selection_multipolygons'
|
||||||
|
task split_geo_area_selection_multipolygons: :environment do
|
||||||
|
puts "Running deploy task 'split_geo_area_selection_multipolygons'"
|
||||||
|
|
||||||
|
Champs::CarteChamp.where.not(value: ['', '[]']).includes(:geo_areas).find_each do |champ|
|
||||||
|
if champ.send(:selection_utilisateur_legacy?)
|
||||||
|
legacy_selection_utilisateur = champ.selections_utilisateur.first
|
||||||
|
champ.send(:legacy_selections_utilisateur).each do |area|
|
||||||
|
champ.geo_areas << area
|
||||||
|
end
|
||||||
|
legacy_selection_utilisateur.destroy
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
AfterParty::TaskRecord.create version: '20200414104712'
|
||||||
|
end
|
||||||
|
end
|
|
@ -13,11 +13,10 @@ describe Champs::CarteController, type: :controller do
|
||||||
champ_id: champ.id
|
champ_id: champ.id
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
let(:geojson) { [] }
|
|
||||||
let(:champ) do
|
let(:champ) do
|
||||||
create(:type_de_champ_carte, options: {
|
create(:type_de_champ_carte, options: {
|
||||||
cadastres: true
|
cadastres: true
|
||||||
}).champ.create(dossier: dossier, value: geojson.to_json)
|
}).champ.create(dossier: dossier)
|
||||||
end
|
end
|
||||||
|
|
||||||
describe 'POST #show' do
|
describe 'POST #show' do
|
||||||
|
@ -31,7 +30,7 @@ describe Champs::CarteController, type: :controller do
|
||||||
|
|
||||||
allow_any_instance_of(ApiCarto::CadastreAdapter)
|
allow_any_instance_of(ApiCarto::CadastreAdapter)
|
||||||
.to receive(:results)
|
.to receive(:results)
|
||||||
.and_return([{ code: "QPCODE1234", surface_parcelle: 4, geometry: { type: "MultiPolygon", coordinates: [[[[2.38715792094576, 48.8723062632126], [2.38724851642619, 48.8721392348061]]]] } }])
|
.and_return([{ code: "QPCODE1234", surface_parcelle: 4, geometry: { type: "MultiPolygon", coordinates: [[[[2.38715792094576, 48.8723062632126], [2.38724851642619, 48.8721392348061], [2.38724851642620, 48.8721392348064], [2.38715792094576, 48.8723062632126]]]] } }])
|
||||||
|
|
||||||
post :show, params: params, format: 'js'
|
post :show, params: params, format: 'js'
|
||||||
end
|
end
|
||||||
|
@ -43,7 +42,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.fire('carte:update', {\"selector\":\".carte-1\",\"data\":{\"position\":{\"lon\":\"2.428462\",\"lat\":\"46.538192\",\"zoom\":\"13\"},\"selection\":null,\"quartiersPrioritaires\":[],\"cadastres\":[],\"parcellesAgricoles\":[]}});")
|
expect(response.body).to include("DS.fire('carte:update'")
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -56,7 +55,6 @@ describe Champs::CarteController, type: :controller do
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when error' do
|
context 'when error' do
|
||||||
let(:geojson) { [[{ "lat": 48.87442541960633, "lng": 2.3859214782714844 }, { "lat": 48.87273183590832, "lng": 2.3850631713867183 }, { "lat": 48.87081237174292, "lng": 2.3809432983398438 }, { "lat": 48.8712640169951, "lng": 2.377510070800781 }, { "lat": 48.87510283703279, "lng": 2.3778533935546875 }, { "lat": 48.87544154230615, "lng": 2.382831573486328 }, { "lat": 48.87442541960633, "lng": 2.3859214782714844 }]] }
|
|
||||||
let(:value) { '' }
|
let(:value) { '' }
|
||||||
|
|
||||||
it {
|
it {
|
||||||
|
|
|
@ -1,9 +1,19 @@
|
||||||
describe Champs::CarteChamp do
|
describe Champs::CarteChamp do
|
||||||
let(:champ) { Champs::CarteChamp.new(value: value) }
|
let(:champ) { Champs::CarteChamp.new(geo_areas: geo_areas) }
|
||||||
let(:value) { '' }
|
let(:value) { '' }
|
||||||
let(:coordinates) { [[{ "lat" => 48.87442541960633, "lng" => 2.3859214782714844 }, { "lat" => 48.87273183590832, "lng" => 2.3850631713867183 }, { "lat" => 48.87081237174292, "lng" => 2.3809432983398438 }, { "lat" => 48.8712640169951, "lng" => 2.377510070800781 }, { "lat" => 48.87510283703279, "lng" => 2.3778533935546875 }, { "lat" => 48.87544154230615, "lng" => 2.382831573486328 }, { "lat" => 48.87442541960633, "lng" => 2.3859214782714844 }]] }
|
let(:coordinates) { [[2.3859214782714844, 48.87442541960633], [2.3850631713867183, 48.87273183590832], [2.3809432983398438, 48.87081237174292], [2.3859214782714844, 48.87442541960633]] }
|
||||||
let(:geo_json_as_string) { GeojsonService.to_json_polygon_for_selection_utilisateur(coordinates) }
|
let(:geo_json) do
|
||||||
let(:geo_json) { JSON.parse(geo_json_as_string) }
|
{
|
||||||
|
"type" => 'Polygon',
|
||||||
|
"coordinates" => coordinates
|
||||||
|
}
|
||||||
|
end
|
||||||
|
let(:legacy_geo_json) do
|
||||||
|
{
|
||||||
|
type: 'MultiPolygon',
|
||||||
|
coordinates: [coordinates]
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
describe '#to_render_data' do
|
describe '#to_render_data' do
|
||||||
subject { champ.to_render_data }
|
subject { champ.to_render_data }
|
||||||
|
@ -18,78 +28,44 @@ describe Champs::CarteChamp do
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
context 'when the value is nil' do
|
context 'when has no geo_areas' do
|
||||||
let(:value) { nil }
|
let(:geo_areas) { [] }
|
||||||
|
|
||||||
let(:selection) { nil }
|
let(:selection) { nil }
|
||||||
|
|
||||||
it { is_expected.to eq(render_data) }
|
it { is_expected.to eq(render_data) }
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when the value is blank' do
|
context 'when has one geo_area' do
|
||||||
let(:value) { '' }
|
let(:geo_areas) { [build(:geo_area, :selection_utilisateur, geometry: geo_json)] }
|
||||||
|
let(:selection) { legacy_geo_json }
|
||||||
let(:selection) { nil }
|
|
||||||
|
|
||||||
it { is_expected.to eq(render_data) }
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when the value is empty array' do
|
|
||||||
let(:value) { '[]' }
|
|
||||||
|
|
||||||
let(:selection) { nil }
|
|
||||||
|
|
||||||
it { is_expected.to eq(render_data) }
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when the value is coordinates' do
|
|
||||||
let(:value) { coordinates.to_json }
|
|
||||||
|
|
||||||
let(:selection) { geo_json }
|
|
||||||
|
|
||||||
it { is_expected.to eq(render_data) }
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when the value is geojson' do
|
|
||||||
let(:value) { geo_json.to_json }
|
|
||||||
|
|
||||||
let(:selection) { geo_json }
|
|
||||||
|
|
||||||
it { is_expected.to eq(render_data) }
|
it { is_expected.to eq(render_data) }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe '#selection_utilisateur_size' do
|
describe '#to_feature_collection' do
|
||||||
subject { champ.selection_utilisateur_size }
|
subject { champ.to_feature_collection }
|
||||||
|
|
||||||
context 'when the value is nil' do
|
let(:feature_collection) {
|
||||||
let(:value) { nil }
|
{
|
||||||
|
type: 'FeatureCollection',
|
||||||
|
bbox: champ.bounding_box,
|
||||||
|
features: features
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
it { is_expected.to eq(0) }
|
context 'when has no geo_areas' do
|
||||||
|
let(:geo_areas) { [] }
|
||||||
|
let(:features) { [] }
|
||||||
|
|
||||||
|
it { is_expected.to eq(feature_collection) }
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when the value is blank' do
|
context 'when has one geo_area' do
|
||||||
let(:value) { '' }
|
let(:geo_areas) { [build(:geo_area, :selection_utilisateur, geometry: geo_json)] }
|
||||||
|
let(:features) { geo_areas.map(&:to_feature) }
|
||||||
|
|
||||||
it { is_expected.to eq(0) }
|
it { is_expected.to eq(feature_collection) }
|
||||||
end
|
|
||||||
|
|
||||||
context 'when the value is empty array' do
|
|
||||||
let(:value) { '[]' }
|
|
||||||
|
|
||||||
it { is_expected.to eq(0) }
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when the value is coordinates' do
|
|
||||||
let(:value) { coordinates.to_json }
|
|
||||||
|
|
||||||
it { is_expected.to eq(1) }
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when the value is geojson' do
|
|
||||||
let(:value) { geo_json.to_json }
|
|
||||||
|
|
||||||
it { is_expected.to eq(1) }
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -27,9 +27,13 @@ describe ChampSerializer do
|
||||||
let(:champ) { create(:champ_carte, value: value, geo_areas: [geo_area].compact) }
|
let(:champ) { create(:champ_carte, value: value, geo_areas: [geo_area].compact) }
|
||||||
let(:value) { nil }
|
let(:value) { nil }
|
||||||
let(:geo_area) { create(:geo_area, geometry: geo_json) }
|
let(:geo_area) { create(:geo_area, geometry: geo_json) }
|
||||||
let(:geo_json_as_string) { GeojsonService.to_json_polygon_for_selection_utilisateur(coordinates) }
|
let(:geo_json) do
|
||||||
let(:geo_json) { JSON.parse(geo_json_as_string) }
|
{
|
||||||
let(:coordinates) { [[{ "lat" => 48.87442541960633, "lng" => 2.3859214782714844 }, { "lat" => 48.87273183590832, "lng" => 2.3850631713867183 }, { "lat" => 48.87081237174292, "lng" => 2.3809432983398438 }, { "lat" => 48.8712640169951, "lng" => 2.377510070800781 }, { "lat" => 48.87510283703279, "lng" => 2.3778533935546875 }, { "lat" => 48.87544154230615, "lng" => 2.382831573486328 }, { "lat" => 48.87442541960633, "lng" => 2.3859214782714844 }]] }
|
"type" => 'MultiPolygon',
|
||||||
|
"coordinates" => coordinates
|
||||||
|
}
|
||||||
|
end
|
||||||
|
let(:coordinates) { [[[2.3859214782714844, 48.87442541960633], [2.3850631713867183, 48.87273183590832], [2.3809432983398438, 48.87081237174292], [2.3859214782714844, 48.87442541960633]]] }
|
||||||
|
|
||||||
let(:serialized_champ) {
|
let(:serialized_champ) {
|
||||||
{
|
{
|
||||||
|
@ -60,19 +64,19 @@ describe ChampSerializer do
|
||||||
context 'when value is nil' do
|
context 'when value is nil' do
|
||||||
let(:value) { nil }
|
let(:value) { nil }
|
||||||
|
|
||||||
it { expect(champ.user_geo_area).to be_nil }
|
it { expect(champ.selection_utilisateur_legacy_geo_area).to be_nil }
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when value is empty array' do
|
context 'when value is empty array' do
|
||||||
let(:value) { '[]' }
|
let(:value) { '[]' }
|
||||||
|
|
||||||
it { expect(champ.user_geo_area).to be_nil }
|
it { expect(champ.selection_utilisateur_legacy_geo_area).to be_nil }
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when value is blank' do
|
context 'when value is blank' do
|
||||||
let(:value) { '' }
|
let(:value) { '' }
|
||||||
|
|
||||||
it { expect(champ.user_geo_area).to be_nil }
|
it { expect(champ.selection_utilisateur_legacy_geo_area).to be_nil }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -80,7 +84,7 @@ describe ChampSerializer do
|
||||||
let(:serialized_libelle) { "user geometry" }
|
let(:serialized_libelle) { "user geometry" }
|
||||||
let(:serialized_type_champ) { "user_geometry" }
|
let(:serialized_type_champ) { "user_geometry" }
|
||||||
|
|
||||||
let(:serializable_object) { champ.user_geo_area }
|
let(:serializable_object) { champ.selection_utilisateur_legacy_geo_area }
|
||||||
|
|
||||||
context 'when value is coordinates' do
|
context 'when value is coordinates' do
|
||||||
let(:value) { coordinates.to_json }
|
let(:value) { coordinates.to_json }
|
||||||
|
@ -167,30 +171,6 @@ describe ChampSerializer do
|
||||||
it { expect(subject).to eq(serialized_champ) }
|
it { expect(subject).to eq(serialized_champ) }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'and geo_area is quartier_prioritaire' do
|
|
||||||
let(:geo_area) { create(:geo_area, :quartier_prioritaire, geometry: geo_json) }
|
|
||||||
|
|
||||||
context 'new_api' do
|
|
||||||
it {
|
|
||||||
expect(subject[:geo_areas].first).to include(
|
|
||||||
source: GeoArea.sources.fetch(:quartier_prioritaire),
|
|
||||||
geometry: geo_json,
|
|
||||||
nom: 'XYZ',
|
|
||||||
commune: 'Paris'
|
|
||||||
)
|
|
||||||
expect(subject[:geo_areas].first.key?(:numero)).to be_falsey
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'old_api' do
|
|
||||||
let(:serializable_object) { champ.geo_areas.first }
|
|
||||||
let(:serialized_libelle) { "quartier prioritaire" }
|
|
||||||
let(:serialized_type_champ) { "quartier_prioritaire" }
|
|
||||||
|
|
||||||
it { expect(subject).to eq(serialized_champ) }
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when type champ is siret' do
|
context 'when type champ is siret' do
|
||||||
|
|
Loading…
Reference in a new issue