Newer
Older

Timothee P
committed
<template>
<div v-frag>
<div class="fourteen wide column">
<h1 v-if="feature && currentRouteName === 'editer-signalement'">
Mise à jour du signalement "{{ feature.title || feature.feature_id }}"
<h1
v-else-if="feature_type && currentRouteName === 'ajouter-signalement'"
Création d'un signalement <small>[{{ feature_type.title }}]</small>

Timothee P
committed
</h1>
<form
id="form-feature-edit"
action=""
method="post"
enctype="multipart/form-data"
class="ui form"
>
<!-- Feature Fields -->
<div class="two fields">
<label :for="form.title.id_for_label">{{ form.title.label }}</label>

Timothee P
committed
<input
:id="form.title.id_for_label"
v-model="form.title.value"

Timothee P
committed
type="text"
required
:maxlength="form.title.field.max_length"
:name="form.title.html_name"

Timothee P
committed
@blur="updateStore"
>
<ul
id="errorlist-title"
class="errorlist"
>
<li
v-for="error in form.title.errors"
:key="error"
>
{{ error }}
</li>
</ul>

Timothee P
committed
</div>
<div class="required field">
<label :for="form.status.id_for_label">{{
form.status.label

Timothee P
committed
}}</label>

Timothee P
committed
:selection.sync="selected_status"

Timothee P
committed
</div>
</div>
<div class="field">
<label :for="form.description.id_for_label">{{
form.description.label

Timothee P
committed
}}</label>
<textarea

Timothee P
committed
rows="5"

Timothee P
committed
@blur="updateStore"

Timothee P
committed
</div>
<!-- Geom Field -->
<div class="field">
<label :for="form.geom.id_for_label">{{ form.geom.label }}</label>

Timothee P
committed
<!-- Import GeoImage -->
<div
v-if="feature_type && feature_type.geom_type === 'point'"
v-frag
>
<p v-if="isOffline() !== true">
<button
id="add-geo-image"
type="button"
class="ui compact button"
<i class="file image icon" />Importer une image géoréférencée
</button>
Vous pouvez utiliser une image géoréférencée pour localiser le
signalement.
</p>

Timothee P
committed
<div
v-if="showGeoRef"
class="ui dimmer modals page transition visible active"
style="display: flex !important"
>
<div
class="ui mini modal transition visible active"
style="display: block !important"
<i
class="close icon"
@click="toggleGeoRefModal"
/>

Timothee P
committed
<div class="content">
<h3>Importer une image géoréférencée</h3>
<form
id="form-geo-image"
class="ui form"
enctype="multipart/form-data"
>
<p>
Attention, si vous avez déjà saisi une géométrie, celle
issue de l'image importée l'écrasera.
</p>
<div class="field georef-btn">
<label>Image (png ou jpeg)</label>
<label
class="ui icon button"
for="image_file"
>
<i class="file icon" />

Timothee P
committed
<span class="label">{{ geoRefFileLabel }}</span>
</label>
<input

Timothee P
committed
type="file"
accept="image/jpeg, image/png"
style="display: none"
name="image_file"
class="image_file"
@change="handleFileUpload"
>
<ul
v-if="erreurUploadMessage"
class="errorlist"
>
<li>
{{ erreurUploadMessage }}
</li>
</ul>

Timothee P
committed
</div>
<button
id="get-geom-from-image-file"
type="button"
:class="[
'ui compact button',
file && !erreurUploadMessage ? 'green' : 'disabled',
{ red: erreurUploadMessage },
]"

Timothee P
committed
>

Timothee P
committed
Importer
</button>
</form>
</div>
</div>

Timothee P
committed
<button
id="create-point-geoposition"
type="button"
class="ui compact button"
<i class="ui map marker alternate icon" />Positionner le
signalement à partir de votre géolocalisation
</button>
</p>
<span
v-if="erreurGeolocalisationMessage"
<div class="ui negative message">
<div class="header">
Une erreur est survenue avec la fonctionnalité de
géolocalisation
</div>
<p id="erreur-geolocalisation-message">
{{ erreurGeolocalisationMessage }}
</p>

Timothee P
committed
</div>
<ul
id="errorlist-geom"
class="errorlist"
>
<li
v-for="error in form.geom.errors"
:key="error"
>
{{ error }}
</li>
</ul>

Timothee P
committed
<!-- Map -->
<input
:id="form.geom.id_for_label"
v-model="form.geom.value"
type="hidden"
:name="form.geom.html_name"

Timothee P
committed
@blur="updateStore"
>
<div
class="ui tab active map-container"
data-tab="map"
>
<div
id="map"
ref="map"
/>
<SidebarLayers v-if="basemaps && map" />

Timothee P
committed
</div>
</div>
<!-- Extra Fields -->
<div class="ui horizontal divider">
DONNÉES MÉTIER
</div>
v-for="(field, index) in orderedCustomFields"
:key="field.field_type + index"
class="field"
>
<FeatureExtraForm :field="field" />

Timothee P
committed
{{ field.errors }}
</div>
<!-- Pièces jointes -->
<div v-if="isOffline() !== true">
<div class="ui horizontal divider">
PIÈCES JOINTES
</div>
<div
v-if="isOffline() !== true"
id="formsets-attachment"
>
<FeatureAttachmentForm
v-for="form in attachmentFormset"
:key="form.dataKey"
ref="attachementForm"

Timothee P
committed
<button
id="add-attachment"
type="button"
class="ui compact basic button button-hover-green"
<i class="ui plus icon" />Ajouter une pièce jointe

Timothee P
committed
<!-- Signalements liés -->
<div v-if="isOffline() !== true">
<div class="ui horizontal divider">
SIGNALEMENTS LIÉS
</div>
<div id="formsets-link">
<FeatureLinkedForm
v-for="form in linkedFormset"
:key="form.dataKey"
ref="linkedForm"
:linked-form="form"
:features="features"
/>
</div>
<button
id="add-link"
type="button"
class="ui compact basic button button-hover-green"
<i class="ui plus icon" />Ajouter une liaison

Timothee P
committed
<button
type="button"
class="ui teal icon button"
@click="postForm"
>
<i class="white save icon" /> Enregistrer les changements

Timothee P
committed
</button>
</form>
</div>
</div>
</template>
<script>
import frag from 'vue-frag';
import { mapState, mapGetters } from 'vuex';
import FeatureAttachmentForm from '@/components/feature/FeatureAttachmentForm';
import FeatureLinkedForm from '@/components/feature/FeatureLinkedForm';
import FeatureExtraForm from '@/components/feature/FeatureExtraForm';
import Dropdown from '@/components/Dropdown.vue';
import SidebarLayers from '@/components/map-layers/SidebarLayers';
import featureAPI from '@/services/feature-api';
import L from 'leaflet';
import 'leaflet-draw';
import { mapUtil } from '@/assets/js/map-util.js';
import axios from '@/axios-client.js';
import flip from '@turf/flip';

Timothee P
committed
// axios.defaults.headers.common["X-CSRFToken"] = ((name) => {
// var re = new RegExp(name + "=([^;]+)");
// var value = re.exec(document.cookie);
// return value !== null ? unescape(value[1]) : null;
// })("csrftoken");

Timothee P
committed
export default {

Timothee P
committed
directives: {
frag,
},
components: {
FeatureAttachmentForm,
FeatureLinkedForm,

Timothee P
committed
SidebarLayers,

Timothee P
committed
data() {
return {
map: null,
baseUrl: this.$store.state.configuration.BASE_URL,
file: null,
showGeoRef: false,
showGeoPositionBtn: true,
erreurGeolocalisationMessage: null,
erreurUploadMessage: null,
attachmentDataKey: 0,
linkedDataKey: 0,

Timothee P
committed
title: {

Timothee P
committed
field: {
max_length: 30,
},
html_name: 'name',
label: 'Nom',
value: '',

Timothee P
committed
},
status: {
id_for_label: 'status',
html_name: 'status',
label: 'Statut',

Timothee P
committed
},
description: {
id_for_label: 'description',
html_name: 'description',
label: 'Description',
value: '',

Timothee P
committed
},
geom: {

Timothee P
committed
},
},
};
},
...mapGetters(['project', 'permissions']),
...mapGetters('feature_type', ['feature_type']),
...mapState(['user', 'USER_LEVEL_PROJECTS']),
...mapState('map', ['basemaps']),
...mapState('feature', [
'attachmentFormset',
'attachmentsToDelete',
'attachmentsToPut',
'linkedFormset',
'features',
'extra_form',
'linked_features',
'statusChoices',
field_title() {
if (this.feature_type) {
if (this.feature_type.title_optional) {
currentRouteName() {
return this.$route.name;
},
Sébastien DA ROCHA
committed
return this.$store.state.feature.current_feature;
return [...this.extra_form].sort((a, b) => a.position - b.position);
selected_status: {
get() {
return this.form.status.value;
},
set(newValue) {
this.form.status.value = newValue;
this.updateStore();
},
},
allowedStatusChoices() {
if (this.project) {
const isModerate = this.project.moderation;
const userStatus = this.USER_LEVEL_PROJECTS[this.project.slug];
const isOwnFeature = this.feature
? this.feature.creator === this.user.id //* prevent undefined feature
: false; //* si le contributeur est l'auteur du signalement
//* si admin, modérateur ou super contributeur, statuts toujours disponible: Brouillon, Publié, Archivé
userStatus === 'Modérateur' ||
userStatus === 'Administrateur projet' ||
(userStatus === 'Super Contributeur' && !isModerate)
return this.statusChoices.filter((el) => el.value !== 'pending');
} else if (userStatus === 'Super Contributeur' && isModerate) {

Timothee P
committed
return this.statusChoices.filter(
(el) => el.value === 'draft' || el.value === 'pending'

Timothee P
committed
);
} else if (userStatus === 'Contributeur') {
//* cas particuliers du contributeur
this.currentRouteName === 'ajouter-signalement' ||
!isOwnFeature
//* même cas à l'ajout d'une feature ou si feature n'a pas été créé par le contributeur
return isModerate
? this.statusChoices.filter(
(el) => el.value === 'draft' || el.value === 'pending'
)
(el) => el.value === 'draft' || el.value === 'published'
);
} else {
//* à l'édition d'une feature et si le contributeur est l'auteur de la feature
return isModerate
? this.statusChoices.filter(
(el) => el.value !== 'published' //* toutes sauf "Publié"
)
: this.statusChoices.filter(
(el) => el.value !== 'pending' //* toutes sauf "En cours de publication"
);
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
created() {
this.$store.commit(
'feature_type/SET_CURRENT_FEATURE_TYPE_SLUG',
this.$route.params.slug_type_signal
);
//* empty previous feature data, not emptying by itself since it doesn't update by itself anymore
if (this.currentRouteName === 'ajouter-signalement') {
this.$store.commit('feature/SET_CURRENT_FEATURE', []);
}
if (this.$route.params.slug_signal) {
this.getFeatureAttachments();
this.getLinkedFeatures();
}
},
mounted() {
let promises = [
this.$store.dispatch('GET_PROJECT_INFO', this.$route.params.slug),
];
if (this.$route.params.slug_signal) {
promises.push(
this.$store.dispatch('feature/GET_PROJECT_FEATURE', {
project_slug: this.$route.params.slug,
feature_id: this.$route.params.slug_signal,
})
);
}
Promise.all(promises).then(() => {
this.initForm();
this.initMap();
this.onFeatureTypeLoaded();
this.initExtraForms(this.feature);
setTimeout(
function () {
mapUtil.addGeocoders(this.$store.state.configuration);
}.bind(this),
1000
);
});
},
destroyed() {
//* be sure that previous Formset have been cleared for creation
this.$store.commit('feature/CLEAR_ATTACHMENT_FORM');
this.$store.commit('feature/CLEAR_LINKED_FORM');
this.$store.commit('feature/CLEAR_EXTRA_FORM');
},

Timothee P
committed
methods: {
isOffline() {
return navigator.onLine == false;
if (this.currentRouteName === 'editer-signalement') {
for (let key in this.feature) {
if (key && this.form[key]) {
const value = this.feature[key];
this.form[key].value = this.statusChoices.find(
(key) => key.value === value
this.form[key].value = this.feature[key];
create_point_geoposition() {
function success(position) {
const latitude = position.coords.latitude;
const longitude = position.coords.longitude;
var layer = L.circleMarker([latitude, longitude]);
this.add_layer_call_back(layer);
this.map.setView([latitude, longitude]);
}
function error(err) {
this.erreurGeolocalisationMessage = err.message;
if (err.message === 'User denied geolocation prompt') {
this.erreurGeolocalisationMessage = null;
this.erreurGeolocalisationMessage =
"La géolocalisation n'est pas supportée par votre navigateur.";
navigator.geolocation.getCurrentPosition(
success.bind(this),
error.bind(this)
);

Timothee P
committed
toggleGeoRefModal() {
if (this.showGeoRef) {
//* when popup closes, empty form

Timothee P
committed
this.file = null;
}
this.showGeoRef = !this.showGeoRef;
},
georeferencement() {
const url = `${this.$store.state.configuration.VUE_APP_DJANGO_API_BASE}exif-geom-reader/`;
let formData = new FormData();
axios
.post(url, formData, {
headers: {
},
})

Timothee P
committed
.then((response) => {
if (response.data.geom.indexOf('POINT') >= 0) {
let regexp = /POINT\s\((.*)\s(.*)\)/;

Timothee P
committed
let arr = regexp.exec(response.data.geom);
coordinates: [parseFloat(arr[1]), parseFloat(arr[2])],

Timothee P
committed
this.updateMap(json);
this.updateGeomField(json);

Timothee P
committed
this.addAttachment({
title: 'Localisation',
info: '',
id: 'loc',

Timothee P
committed
attachment_file: this.file.name,
fileToImport: this.file,
if (error && error.response && error.response) {
this.erreurUploadMessage = error.response.data.error;
} else {
this.erreurUploadMessage =
"Une erreur est survenue pendant l'import de l'image géoréférencée";
}
initExtraForms(feature) {
function findCurrentValue(label) {
const field = feature.feature_data.find((el) => el.label === label);
return field ? field.value : null;
let extraForm = this.feature_type.customfield_set.map((field) => {
return {
...field,
//* add value field to extra forms from feature_type and existing values if feature is defined
value:
feature && feature.feature_data
? findCurrentValue(field.label)
: null,
this.$store.commit('feature/SET_EXTRA_FORM', extraForm);
this.$store.commit('feature/ADD_ATTACHMENT_FORM', {
dataKey: this.attachmentDataKey,
}); // * create an object with the counter in store

Timothee P
committed
this.attachmentDataKey += 1; // * increment counter for key in v-for
this.$store.commit('feature/ADD_ATTACHMENT_FORM', {
dataKey: this.attachmentDataKey,
title: attachment.title,
attachment_file: attachment.attachment_file,
info: attachment.info,
fileToImport: attachment.fileToImport,
id: attachment.id,
});
this.attachmentDataKey += 1;
addExistingAttachementFormset(attachementFormset) {
for (const attachment of attachementFormset) {
this.addAttachment(attachment);
}
},
this.$store.commit('feature/ADD_LINKED_FORM', {

Timothee P
committed
dataKey: this.linkedDataKey,
}); // * create an object with the counter in store
this.linkedDataKey += 1; // * increment counter for key in v-for

Timothee P
committed
addExistingLinkedFormset(linkedFormset) {
for (const linked of linkedFormset) {
this.$store.commit('feature/ADD_LINKED_FORM', {

Timothee P
committed
dataKey: this.linkedDataKey,
relation_type: linked.relation_type,
feature_to: linked.feature_to,
});
this.linkedDataKey += 1;
}
},

Timothee P
committed
updateStore() {
this.$store.commit('feature/UPDATE_FORM', {
title: this.form.title.value,
status: this.form.status.value,
description: this.form.description,
geometry: this.form.geom.value,
feature_id: this.feature ? this.feature.feature_id : '',

Timothee P
committed
});
checkFormTitle() {
this.form.title.errors = [];
!this.form.title.errors.includes('Veuillez compléter ce champ.')
this.form.title.errors.push('Veuillez compléter ce champ.');
.getElementById('errorlist-title')
.scrollIntoView({ block: 'end', inline: 'nearest' });
}
return false;
},
checkFormGeom() {
if (this.form.geom.value) {
this.form.geom.errors = [];
return true;
} else if (
!this.form.geom.errors.includes('Valeur géométrique non valide.')
this.form.geom.errors.push('Valeur géométrique non valide.');
.getElementById('errorlist-geom')
.scrollIntoView({ block: 'end', inline: 'nearest' });
checkAddedForm() {
let isValid = true; //* fallback if all customForms returned true
if (this.$refs.attachementForm) {
for (const attachementForm of this.$refs.attachementForm) {
if (attachementForm.checkForm() === false) {
isValid = false;
}
}
}
if (this.$refs.linkedForm) {
for (const linkedForm of this.$refs.linkedForm) {
if (linkedForm.checkForm() === false) {
isValid = false;
}
}
}
return isValid;
},
let is_valid = true;
if (!this.feature_type.title_optional) {
is_valid =
this.checkFormTitle() &&
this.checkFormGeom() &&
this.checkAddedForm();
} else {
is_valid = this.checkFormGeom() && this.checkAddedForm();
//* in a moderate project, at edition of a published feature by someone else than admin or moderator, switch published status to draft.
if (
this.project.moderation &&
this.currentRouteName === 'editer-signalement' &&
this.form.status.value.value === 'published' &&
!this.permissions.is_project_administrator &&
!this.permissions.is_project_moderator
this.form.status.value = { name: 'Brouillon', value: 'draft' };
this.$store.dispatch('feature/SEND_FEATURE', this.currentRouteName);
//* ************* MAP *************** *//
onFeatureTypeLoaded() {
point: 'circlemarker',
linestring: 'polyline',
polygon: 'polygon',
var drawConfig = {
polygon: false,
marker: false,
polyline: false,
rectangle: false,
circle: false,
circlemarker: false,
};
drawConfig[geomLeaflet[geomType]] = true;
L.drawLocal = {
draw: {
toolbar: {
actions: {
title: 'Annuler le dessin',
text: 'Annuler',
title: 'Terminer le dessin',
text: 'Terminer',
title: 'Supprimer le dernier point dessiné',
text: 'Supprimer le dernier point',
polyline: 'Dessiner une polyligne',
polygon: 'Dessiner un polygone',
rectangle: 'Dessiner un rectangle',
circle: 'Dessiner un cercle',
marker: 'Dessiner une balise',
circlemarker: 'Dessiner un point',
start: 'Cliquer et glisser pour dessiner le cercle.',
start: 'Cliquer sur la carte pour placer le point.',
start: 'Cliquer sur la carte pour placer la balise.',
start: 'Cliquer pour commencer à dessiner.',
cont: 'Cliquer pour continuer à dessiner.',
end: 'Cliquer sur le premier point pour terminer le dessin.',
error: '<strong>Error:</strong> shape edges cannot cross!',
start: 'Cliquer pour commencer à dessiner.',
cont: 'Cliquer pour continuer à dessiner.',
end: 'Cliquer sur le dernier point pour terminer le dessin.',
start: 'Cliquer et glisser pour dessiner le rectangle.',
end: 'Relâcher la souris pour terminer de dessiner.',
},
edit: {
toolbar: {
actions: {
save: {
title: 'Sauver les modifications',
text: 'Sauver',
'Annuler la modification, annule toutes les modifications',
text: 'Annuler',
title: "Effacer l'objet",
edit: "Modifier l'objet",
remove: "Supprimer l'objet",
removeDisabled: 'Aucun objet à supprimer',
text: "Faites glisser les marqueurs ou les balises pour modifier l'élément.",
subtext: 'Cliquez sur Annuler pour annuler les modifications..',
text: 'Cliquez sur un élément pour le supprimer.',
},
},
},
},
this.drawnItems = new L.FeatureGroup();
this.map.addLayer(this.drawnItems);
this.drawControlFull = new L.Control.Draw({
featureGroup: this.drawnItems,
this.drawControlEditOnly = new L.Control.Draw({
featureGroup: this.drawnItems,
if (this.currentRouteName === 'editer-signalement') {
} else {
this.map.addControl(this.drawControlFull);
}
this.changeMobileBtnOrder();
this.map.on(
function (e) {
var layer = e.layer;
this.add_layer_call_back(layer);
}.bind(this)
);
//var wellknown;// TODO Remplacer par autre chose
function (e) {
var layers = e.layers;
let self = this;
layers.eachLayer(function (layer) {
//this.updateGeomField(wellknown.stringify(layer.toGeoJSON()))
self.updateGeomField(layer.toGeoJSON());
});
}.bind(this)
);
this.map.on(
function () {
this.drawControlEditOnly.remove(this.map);
this.drawControlFull.addTo(this.map);
this.updateGeomField('');
if (geomType === 'point') {
this.showGeoPositionBtn = true;
}
}.bind(this)
);
updateMap(geomFeatureJSON) {
if (this.drawnItems) this.drawnItems.clearLayers();
var geomType = this.feature_type.geom_type;
if (geomFeatureJSON) {
var geomJSON = flip(geomFeatureJSON.geometry); //turf.flip(geomFeatureJSON)
L.circleMarker(geomJSON.coordinates).addTo(this.drawnItems);
L.polyline(geomJSON.coordinates).addTo(this.drawnItems);
L.polygon(geomJSON.coordinates).addTo(this.drawnItems);
this.map.fitBounds(this.drawnItems.getBounds(), { padding: [25, 25] });
this.map.setView(
this.$store.state.configuration.DEFAULT_MAP_VIEW.center,
this.$store.state.configuration.DEFAULT_MAP_VIEW.zoom
);
updateGeomField(newGeom) {
this.form.geom.value = newGeom.geometry;