Merge pull request #5129 from betagouv/dev

2020-05-11-01
This commit is contained in:
krichtof 2020-05-11 14:54:21 +02:00 committed by GitHub
commit 1e720f7587
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 390 additions and 109 deletions

View file

@ -40,6 +40,54 @@ class Champs::CarteController < ApplicationController
response.status = 503
end
def index
@selector = ".carte-#{params[:champ_id]}"
@champ = policy_scope(Champ).find(params[:champ_id])
@update_cadastres = params[:cadastres]
if @champ.cadastres? && @update_cadastres
@champ.geo_areas.cadastres.destroy_all
@champ.geo_areas += GeoArea.from_feature_collection(cadastres_features_collection(@champ.to_feature_collection))
@champ.save!
end
rescue ApiCarto::API::ResourceNotFound
flash.alert = 'Les données cartographiques sont temporairement indisponibles. Réessayez dans un instant.'
response.status = 503
end
def create
champ = policy_scope(Champ).find(params[:champ_id])
geo_area = champ.geo_areas.selections_utilisateur.new
save_geometry!(geo_area, params_feature)
render json: { feature: geo_area.to_feature }, status: :created
end
def update
champ = policy_scope(Champ).find(params[:champ_id])
geo_area = champ.geo_areas.selections_utilisateur.find(params[:id])
save_geometry!(geo_area, params_feature)
head :no_content
end
def destroy
champ = policy_scope(Champ).find(params[:champ_id])
champ.geo_areas.selections_utilisateur.find(params[:id]).destroy!
head :no_content
end
def import
champ = policy_scope(Champ).find(params[:champ_id])
params_features.each do |feature|
geo_area = champ.geo_areas.selections_utilisateur.new
save_geometry!(geo_area, feature)
end
render json: champ.to_feature_collection, status: :created
end
private
def populate_cadastres(feature_collection)
@ -61,4 +109,45 @@ class Champs::CarteController < ApplicationController
end
end
end
def params_feature
params[:feature]
end
def params_features
params[:features]
end
def save_geometry!(geo_area, feature)
geo_area.geometry = feature[:geometry]
geo_area.save!
end
def cadastres_features_collection(feature_collection)
coordinates = feature_collection[:features].filter do |feature|
feature[:properties][:source] == GeoArea.sources.fetch(:selection_utilisateur) && feature[:geometry]['type'] == 'Polygon'
end.map do |feature|
feature[:geometry]['coordinates'][0].map { |(lng, lat)| { 'lng' => lng, 'lat' => lat } }
end
if coordinates.present?
cadastres = ApiCartoService.generate_cadastre(coordinates)
{
type: 'FeatureCollection',
features: cadastres.map do |cadastre|
{
type: 'Feature',
geometry: cadastre.delete(:geometry),
properties: cadastre.merge(source: GeoArea.sources.fetch(:cadastre))
}
end
}
else
{
type: 'FeatureCollection',
features: []
}
end
end
end

View file

@ -72,6 +72,14 @@ module Instructeurs
end
end
def bilans_bdf
if avis.dossier.etablissement&.entreprise_bilans_bdf_to_csv.present?
render csv: avis.dossier.etablissement.entreprise_bilans_bdf_to_csv
else
redirect_to instructeur_avis_path(avis)
end
end
def sign_up
@email = params[:email]
@dossier = Avis.includes(:dossier).find(params[:id]).dossier

View file

@ -692,6 +692,8 @@ type Effectif {
}
type Entreprise {
attestationFiscaleAttachment: File
attestationSocialeAttachment: File
capitalSocial: BigInt!
codeEffectifEntreprise: String!
dateCreation: ISO8601Date!

View file

@ -21,6 +21,16 @@ module Types
field :nom, String, null: false
field :prenom, String, null: false
field :inline_adresse, String, null: false
field :attestation_sociale_attachment, Types::File, null: true
field :attestation_fiscale_attachment, Types::File, null: true
def attestation_sociale_attachment
load_attachment_for(:entreprise_attestation_sociale_attachment)
end
def attestation_fiscale_attachment
load_attachment_for(:entreprise_attestation_fiscale_attachment)
end
def effectif_mensuel
if object.effectif_mensuel.present?
@ -39,6 +49,15 @@ module Types
}
end
end
private
def load_attachment_for(key)
Loaders::Association.for(
Etablissement,
key => :blob
).load(object.etablissement)
end
end
class AssociationType < Types::BaseObject

View file

@ -3,116 +3,122 @@ import PropTypes from 'prop-types';
import mapboxgl from 'mapbox-gl';
import ReactMapboxGl, { GeoJSONLayer, ZoomControl } from 'react-mapbox-gl';
import DrawControl from 'react-mapbox-gl-draw';
import area from '@turf/area';
import SwitchMapStyle from './SwitchMapStyle';
import SearchInput from './SearchInput';
import { fire } from '@utils';
import { getJSON, ajax } from '@utils';
import { gpx } from '@tmcw/togeojson/dist/togeojson.es.js';
import ortho from './styles/ortho.json';
import vector from './styles/vector.json';
import {
createFeatureCollection,
polygonCadastresFill,
polygonCadastresLine,
ERROR_GEO_JSON
} from './utils';
import { polygonCadastresFill, polygonCadastresLine } from './utils';
import '@mapbox/mapbox-gl-draw/dist/mapbox-gl-draw.css';
const Map = ReactMapboxGl({});
const MapEditor = ({ featureCollection: { features, bbox, id } }) => {
function filterFeatureCollection(featureCollection, source) {
return {
type: 'FeatureCollection',
features: featureCollection.features.filter(
feature => feature.properties.source === source
)
};
}
const MapEditor = ({ featureCollection, url }) => {
const drawControl = useRef(null);
const [style, setStyle] = useState('ortho');
const [coords, setCoords] = useState([1.7, 46.9]);
const [zoom, setZoom] = useState([5]);
const [currentMap, setCurrentMap] = useState({});
let input = document.querySelector(
`input[data-feature-collection-id="${id}"]`
);
let userSelections = features.filter(
feature => feature.properties.source === 'selection_utilisateur'
);
let cadastresFeatureCollection = {
type: 'FeatureCollection',
features: []
};
const constructCadastresFeatureCollection = features => {
for (let feature of features) {
switch (feature.properties.source) {
case 'cadastre':
cadastresFeatureCollection.features.push(feature);
break;
}
}
};
constructCadastresFeatureCollection(features);
const [bbox, setBbox] = useState(featureCollection.bbox);
const mapStyle = style === 'ortho' ? ortho : vector;
const saveFeatureCollection = featuresToSave => {
const featuresCollection = createFeatureCollection(featuresToSave);
if (area(featuresCollection) < 300000) {
input.value = JSON.stringify(featuresCollection);
} else {
input.value = ERROR_GEO_JSON;
}
fire(input, 'change');
};
const onDrawCreate = ({ features }) => {
const draw = drawControl.current.draw;
const featureId = features[0].id;
draw.setFeatureProperty(featureId, 'id', featureId);
draw.setFeatureProperty(featureId, 'source', 'selection_utilisateur');
userSelections.push(draw.get(featureId));
saveFeatureCollection(userSelections);
};
const onDrawUpdate = ({ features }) => {
let featureId = features[0].properties.id;
userSelections = userSelections.map(selection => {
if (selection.properties.id === featureId) {
selection = features[0];
}
return selection;
});
saveFeatureCollection(userSelections);
};
const onDrawDelete = ({ features }) => {
userSelections = userSelections.filter(
selection => selection.properties.id !== features[0].properties.id
const cadastresFeatureCollection = filterFeatureCollection(
featureCollection,
'cadastre'
);
saveFeatureCollection(userSelections);
};
function updateFeaturesList(features) {
const cadastres = features.find(
({ geometry }) => geometry.type === 'Polygon'
);
ajax({ url, type: 'get', data: cadastres ? 'cadastres=update' : '' });
}
function setFeatureId(lid, feature) {
const draw = drawControl.current.draw;
draw.setFeatureProperty(lid, 'id', feature.properties.id);
}
async function onDrawCreate({ features }) {
for (const feature of features) {
const data = await getJSON(url, { feature }, 'post');
setFeatureId(feature.id, data.feature);
}
updateFeaturesList(features);
}
async function onDrawUpdate({ features }) {
for (const feature of features) {
let { id } = feature.properties;
await getJSON(`${url}/${id}`, { feature }, 'patch');
}
updateFeaturesList(features);
}
async function onDrawDelete({ features }) {
for (const feature of features) {
const { id } = feature.properties;
await getJSON(`${url}/${id}`, null, 'delete');
}
updateFeaturesList(features);
}
const onMapLoad = map => {
setCurrentMap(map);
if (userSelections.length > 0) {
userSelections.map((selection, index) => {
selection.properties.id = index + 1;
drawControl.current.draw.add(selection);
});
drawControl.current.draw.set(
filterFeatureCollection(featureCollection, 'selection_utilisateur')
);
};
const onCadastresUpdate = evt => {
if (currentMap) {
currentMap
.getSource('cadastres-layer')
.setData(
filterFeatureCollection(evt.detail.featureCollection, 'cadastre')
);
}
};
const onMapUpdate = evt => {
if (currentMap) {
cadastresFeatureCollection.features = [];
constructCadastresFeatureCollection(
evt.detail.featureCollection.features
const onGpxImport = e => {
let reader = new FileReader();
reader.readAsText(e.target.files[0], 'UTF-8');
reader.onload = async event => {
const featureCollection = gpx(
new DOMParser().parseFromString(event.target.result, 'text/xml')
);
currentMap
.getSource('cadastres-layer')
.setData(cadastresFeatureCollection);
}
const resultFeatureCollection = await getJSON(
`${url}/import`,
featureCollection,
'post'
);
drawControl.current.draw.set(
filterFeatureCollection(
resultFeatureCollection,
'selection_utilisateur'
)
);
updateFeaturesList(resultFeatureCollection.features);
setBbox(resultFeatureCollection.bbox);
};
};
useEffect(() => {
addEventListener('map:update', onMapUpdate);
return () => removeEventListener('map:update', onMapUpdate);
addEventListener('cadastres:update', onCadastresUpdate);
return () => removeEventListener('cadastres:update', onCadastresUpdate);
});
if (!mapboxgl.supported()) {
@ -127,6 +133,16 @@ const MapEditor = ({ featureCollection: { features, bbox, id } }) => {
return (
<>
<div className="file-import" style={{ marginBottom: '20px' }}>
<div>
<p style={{ fontWeight: 'bolder', marginBottom: '10px' }}>
Importer un fichier GPX
</p>
</div>
<div>
<input type="file" accept=".gpx" onChange={onGpxImport} />
</div>
</div>
<div
style={{
marginBottom: '62px'
@ -193,7 +209,8 @@ MapEditor.propTypes = {
bbox: PropTypes.array,
features: PropTypes.array,
id: PropTypes.number
})
}),
url: PropTypes.string
};
export default MapEditor;

View file

@ -47,7 +47,7 @@ const SearchInput = ({ getCoords }) => {
return (
<Combobox aria-label="addresses">
<ComboboxInput
placeholder="Saisissez au moins 2 caractères"
placeholder="Rechercher une adresse : saisissez au moins 2 caractères"
className="address-search-input"
style={{
font: 'inherit',

View file

@ -1,11 +1,3 @@
export const ERROR_GEO_JSON = '';
export const createFeatureCollection = selectionsUtilisateur => {
return {
type: 'FeatureCollection',
features: selectionsUtilisateur
};
};
export const polygonCadastresFill = {
'fill-color': '#EC3323',
'fill-opacity': 0.3

View file

@ -68,7 +68,7 @@ export function ajax(options) {
}
export function getJSON(url, data, method = 'get') {
data = method !== 'get' ? JSON.stringify(data) : data;
data = method !== 'get' && data ? JSON.stringify(data) : data;
return Promise.resolve(
$.ajax({
method,

View file

@ -3,6 +3,7 @@ class Entreprise < Hashie::Dash
self[attribute]
end
property :etablissement
property :siren
property :capital_social
property :numero_tva_intracommunautaire

View file

@ -96,6 +96,7 @@ class Etablissement < ApplicationRecord
def entreprise
Entreprise.new(
etablissement: self,
siren: entreprise_siren,
capital_social: entreprise_capital_social,
numero_tva_intracommunautaire: entreprise_numero_tva_intracommunautaire,

View file

@ -0,0 +1,9 @@
<%= render_flash(timeout: 5000, fixed: true) %>
<%= render_to_element("#{@selector} + .geo-areas",
partial: 'shared/champs/carte/geo_areas',
locals: { champ: @champ, error: @error }) %>
<% if @update_cadastres %>
<%= fire_event('cadastres:update', { featureCollection: @champ.to_feature_collection }.to_json) %>
<% end %>

View file

@ -4,8 +4,4 @@
partial: 'shared/champs/carte/geo_areas',
locals: { champ: @champ, error: @error }) %>
<% if feature_enabled?(:new_map_editor) %>
<%= fire_event('map:update', { featureCollection: @champ.to_feature_collection }.to_json) %>
<% else %>
<%= fire_event('carte:update', { selector: @selector, data: @champ.to_render_data }.to_json) %>
<% end %>

View file

@ -57,7 +57,11 @@ def render_identite_etablissement(pdf, etablissement)
pdf.text " - Libellé NAF : #{etablissement.libelle_naf}"
pdf.text " - Code NAF : #{etablissement.naf}"
pdf.text " - Date de création : #{try_format_date(etablissement.entreprise.date_creation)}"
pdf.text " - Effectif de l'organisation : #{effectif(etablissement)}"
if @include_infos_administration
pdf.text " - Effectif mensuel #{try_format_mois_effectif(etablissement)} (URSSAF) : #{etablissement.entreprise_effectif_mensuel}"
pdf.text " - Effectif moyen annuel #{etablissement.entreprise_effectif_annuel_annee} (URSSAF) : #{etablissement.entreprise_effectif_annuel}"
end
pdf.text " - Effectif de l'organisation (INSEE) : #{effectif(etablissement)}"
pdf.text " - Code effectif : #{etablissement.entreprise.code_effectif_entreprise}"
pdf.text " - Numéro de TVA intracommunautaire : #{etablissement.entreprise.numero_tva_intracommunautaire}"
pdf.text " - Adresse : #{etablissement.adresse}"

View file

@ -83,7 +83,10 @@
%th.libelle
Bilans Banque de France
= "en #{etablissement.entreprise_bilans_bdf_monnaie}"
%td= link_to "Consulter les bilans", bilans_bdf_instructeur_dossier_path
- if controller.is_a?(Instructeurs::AvisController)
%td= link_to "Consulter les bilans", bilans_bdf_instructeur_avis_path(@avis.id)
- else
%td= link_to "Consulter les bilans", bilans_bdf_instructeur_dossier_path(procedure_id: @dossier.procedure.id, dossier_id: @dossier.id)
- if etablissement.association?
%tr

View file

@ -1,13 +1,13 @@
- if feature_enabled?(:new_map_editor)
= react_component("MapEditor", { featureCollection: champ.to_feature_collection }, class: "carte-#{form.index}")
= react_component("MapEditor", { featureCollection: champ.to_feature_collection, url: champs_carte_features_path(champ) }, class: "carte-#{champ.id}")
- else
.toolbar
%button.button.primary.new-area Ajouter une zone
%select.select2.adresse{ data: { address: true }, placeholder: 'Saisissez une adresse ou positionner la carte' }
.carte.edit{ data: { geo: geo_data(champ) }, class: "carte-#{form.index}" }
.geo-areas
= render partial: 'shared/champs/carte/geo_areas', locals: { champ: champ, error: false }
= form.hidden_field :value,
data: { remote: true, feature_collection_id: champ.stable_id, url: champs_carte_path(form.index), params: champ_carte_params(champ).to_query, method: 'post' }
.geo-areas
= render partial: 'shared/champs/carte/geo_areas', locals: { champ: champ, error: false }

View file

@ -120,6 +120,13 @@ Rails.application.routes.draw do
get ':position/siret', to: 'siret#show', as: :siret
get ':position/dossier_link', to: 'dossier_link#show', as: :dossier_link
post ':position/carte', to: 'carte#show', as: :carte
get ':champ_id/carte/features', to: 'carte#index', as: :carte_features
post ':champ_id/carte/features', to: 'carte#create'
post ':champ_id/carte/features/import', to: 'carte#import'
patch ':champ_id/carte/features/:id', to: 'carte#update'
delete ':champ_id/carte/features/:id', to: 'carte#destroy'
post ':position/repetition', to: 'repetition#show', as: :repetition
put 'piece_justificative/:champ_id', to: 'piece_justificative#update', as: :piece_justificative
end
@ -347,6 +354,7 @@ Rails.application.routes.draw do
get 'messagerie'
post 'commentaire' => 'avis#create_commentaire'
post 'avis' => 'avis#create_avis'
get 'bilans_bdf'
get 'sign_up/email/:email' => 'avis#sign_up', constraints: { email: /.*/ }, as: 'sign_up'
post 'sign_up/email/:email' => 'avis#create_instructeur', constraints: { email: /.*/ }

View file

@ -11,6 +11,7 @@
"@rails/webpacker": "4.2.2",
"@reach/combobox": "^0.10.0",
"@sentry/browser": "^5.11.2",
"@tmcw/togeojson": "^4.0.0",
"@turf/area": "^6.0.1",
"babel-plugin-macros": "^2.8.0",
"babel-plugin-transform-react-remove-prop-types": "^0.4.24",

View file

@ -18,6 +18,126 @@ describe Champs::CarteController, type: :controller do
cadastres: true
}).champ.create(dossier: dossier)
end
describe 'features' do
let(:feature) { attributes_for(:geo_area, :polygon) }
let(:geo_area) { create(:geo_area, :selection_utilisateur, :polygon, champ: champ) }
let(:params) do
{
champ_id: champ.id,
feature: feature
}
end
before do
sign_in user
request.accept = "application/json"
request.content_type = "application/json"
end
describe 'POST #create' do
before do
post :create, params: params
end
it { expect(response.status).to eq 201 }
end
describe 'PATCH #update' do
let(:params) do
{
champ_id: champ.id,
id: geo_area.id,
feature: feature
}
end
before do
patch :update, params: params
end
it { expect(response.status).to eq 204 }
end
describe 'DELETE #destroy' do
let(:params) do
{
champ_id: champ.id,
id: geo_area.id
}
end
before do
delete :destroy, params: params
end
it { expect(response.status).to eq 204 }
end
describe 'POST #import' do
render_views
let(:params) do
{
champ_id: champ.id,
features: [feature]
}
end
before do
post :import, params: params
end
it {
expect(response.status).to eq 201
expect(response.body).to include("bbox")
}
end
describe 'GET #index' do
render_views
before do
request.accept = "application/javascript"
request.content_type = "application/javascript"
end
context 'with cadastres update' do
let(:params) do
{
champ_id: champ.id,
cadastres: 'update'
}
end
before do
get :index, params: params
end
it {
expect(response.status).to eq 200
expect(response.body).to include("DS.fire('cadastres:update'")
}
end
context 'without cadastres update' do
let(:params) do
{
champ_id: champ.id
}
end
before do
get :index, params: params
end
it {
expect(response.status).to eq 200
expect(response.body).not_to include("DS.fire('cadastres:update'")
}
end
end
end
describe 'POST #show' do
render_views

View file

@ -50,6 +50,12 @@ describe Instructeurs::AvisController, type: :controller do
it { expect(assigns(:dossier)).to eq(dossier) }
end
describe '#bilans_bdf' do
before { get :bilans_bdf, params: { id: avis_without_answer.id } }
it { expect(response).to redirect_to(instructeur_avis_path(avis_without_answer)) }
end
describe '#update' do
describe 'without attachment' do
before do

View file

@ -133,13 +133,13 @@ describe DossierSearchService do
context 'when the user owns the dossier' do
let(:terms) { dossier_0.id.to_s }
it { expect(subject.size).to eq(1) }
it { expect(subject.map(&:id)).to include(dossier_0.id) }
end
context 'when the user does not own the dossier' do
let(:terms) { dossier_0b.id.to_s }
it { expect(subject.size).to eq(0) }
it { expect(subject.map(&:id)).not_to include(dossier_0b.id) }
end
end

View file

@ -1263,6 +1263,11 @@
"@sentry/types" "5.15.4"
tslib "^1.9.3"
"@tmcw/togeojson@^4.0.0":
version "4.0.0"
resolved "https://registry.yarnpkg.com/@tmcw/togeojson/-/togeojson-4.0.0.tgz#ee111e4e5b2b5d498d43e27a6c9bf546ce4041cc"
integrity sha512-JZXGC1myBPPYb/moq03cYPtErqZKzVR74Cv9C85IuqATHCxHCNOxw4D45vVcYHQnnxG2TQTIR+igzpbFiu/O6Q==
"@turf/area@^6.0.1":
version "6.0.1"
resolved "https://registry.yarnpkg.com/@turf/area/-/area-6.0.1.tgz#50ed63c70ef2bdb72952384f1594319d94f3b051"