diff --git a/src/assets/js/map-util.js b/src/assets/js/map-util.js index 7b9c606cd682670d038a9f99ec707ea84071f360..2bb0f78e4a4cc104f50751ca06cd87412feaf234 100644 --- a/src/assets/js/map-util.js +++ b/src/assets/js/map-util.js @@ -3,6 +3,13 @@ import "leaflet/dist/leaflet.css"; import flip from '@turf/flip' import axios from "axios" +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'); + + let map; let dictLayersToLeaflet = {}; diff --git a/src/components/ImportTask.vue b/src/components/ImportTask.vue index fb642ab722c761cc8c80988e6413576d0fd1f9e7..d7e008942c59ba2405acd96ee2611f5d5f0e4b58 100644 --- a/src/components/ImportTask.vue +++ b/src/components/ImportTask.vue @@ -23,7 +23,7 @@ <td> <span v-if="importFile.infos" - :data-tooltip="dataTooltipMsg(importFile.infos)" + :data-tooltip="importFile.infos" class="ui icon" > <i diff --git a/src/components/feature/FeatureAttachmentForm.vue b/src/components/feature/FeatureAttachmentForm.vue index 7e9ef3a1d2aa912bb6292cd6e6c8c7c82ddec024..c75905f2db595f7cee272a5d245ede78a522024f 100644 --- a/src/components/feature/FeatureAttachmentForm.vue +++ b/src/components/feature/FeatureAttachmentForm.vue @@ -1,21 +1,17 @@ <template> <div> - <!-- <span v-for="hidden in form.hidden_fields" :key="hidden"> - {{ hidden }} - </span> --> - <div class="ui teal segment"> <h4> Pièce jointe <button - @click="remove_attachment_formset(form.dataKey)" + @click="removeAttachmentFormset(form.dataKey)" class="ui small compact right floated icon button remove-formset" type="button" > <i class="ui times icon"></i> </button> </h4> - {{ form.errors }} + <!-- {{ form.errors }} --> <div class="visible-fields"> <div class="two fields"> <div class="required field"> @@ -27,35 +23,38 @@ :name="form.title.html_name" :id="form.title.id_for_label" v-model="form.title.value" - @blur="updateStore" + @change="updateStore" /> - {{ form.title.errors }} + <ul :id="form.title.id_for_error" class="errorlist"> + <li v-for="error in form.title.errors" :key="error"> + {{ error }} + </li> + </ul> </div> <div class="required field"> <label>Fichier (PDF, PNG, JPEG)</label> - <!-- // todo : mettre en place la sélection de fichier --> <label - @click="selectFile" class="ui icon button" - :for="form.attachment_file.id_for_label" + :for="'attachment_file' + attachmentForm.dataKey" > <i class="file icon"></i> <span v-if="form.attachment_file.value" class="label">{{ form.attachment_file.value }}</span> - <span v-else class="label">Sélectionner un fichier ...</span> + <span v-else class="label">Sélectionner un fichier ... </span> </label> - <!-- // todo: récupérer la valeur :accept="IMAGE_FORMAT" --> - <!-- @change="processImgData" --> <input + @change="onFileChange" type="file" style="display: none" :name="form.attachment_file.html_name" - class="image_file" - :id="form.attachment_file.id_for_label" - @blur="updateStore" + :id="'attachment_file' + attachmentForm.dataKey" /> - {{ form.attachment_file.errors }} + <ul :id="form.attachment_file.id_for_error" class="errorlist"> + <li v-for="error in form.attachment_file.errors" :key="error"> + {{ error }} + </li> + </ul> </div> </div> <div class="field"> @@ -64,9 +63,9 @@ name="form.info.html_name" rows="5" v-model="form.info.value" - @blur="updateStore" + @change="updateStore" ></textarea> - {{ form.info.errors }} + <!-- {{ form.info.errors }} --> </div> </div> </div> @@ -81,9 +80,11 @@ export default { data() { return { + fileToImport: null, form: { title: { - errors: null, + errors: [], + id_for_error: `errorlist-title-${this.attachmentForm.dataKey}`, id_for_label: "titre", field: { max_length: 30, // todo : vérifier dans django @@ -93,11 +94,11 @@ export default { value: "", }, attachment_file: { - errors: null, - id_for_label: "titre", + errors: [], + id_for_error: `errorlist-file-${this.attachmentForm.dataKey}`, html_name: "titre", label: "Titre", - value: "", + value: null, }, info: { value: "", @@ -107,22 +108,76 @@ export default { }, }; }, + + watch: { + attachmentForm(newValue) { + this.initForm(newValue); + }, + }, + methods: { - remove_attachment_formset() { + initForm(attachmentForm) { + for (let el in attachmentForm) { + if (el && this.form[el]) { + if (el === "attachment_file" && attachmentForm[el]) { + this.form[el].value = attachmentForm[el].split("/").pop(); //* keep only the file name, not the path + } else { + this.form[el].value = attachmentForm[el]; + } + } + } + }, + + removeAttachmentFormset() { this.$store.commit( "feature/REMOVE_ATTACHMENT_FORM", this.attachmentForm.dataKey ); }, - selectFile() {}, + updateStore() { this.$store.commit("feature/UPDATE_ATTACHMENT_FORM", { dataKey: this.attachmentForm.dataKey, title: this.form.title.value, attachment_file: this.form.attachment_file.value, info: this.form.info.value, + fileToImport: this.fileToImport, }); }, + + onFileChange(e) { + const files = e.target.files || e.dataTransfer.files; + if (!files.length) return; + this.fileToImport = files[0]; //* store file to import + this.form.attachment_file.value = files[0].name; //* add name to the form for display, in order to match format return from API + this.updateStore(); + }, + + checkForm() { + let isValid = true; + if (this.form.title.value === "") { + this.form.title.errors = ["Veuillez compléter ce champ."]; + document + .getElementById(this.form.title.id_for_error) + .scrollIntoView({ block: "start", inline: "nearest" }); + isValid = false; + } else if (this.form.attachment_file.value === null) { + this.form.attachment_file.errors = ["Veuillez compléter ce champ."]; + this.form.title.errors = []; + document + .getElementById(this.form.attachment_file.id_for_error) + .scrollIntoView({ block: "start", inline: "nearest" }); + isValid = false; + } else { + this.form.title.errors = []; + this.form.attachment_file.errors = []; + } + return isValid; + }, + }, + + mounted() { + this.initForm(this.attachmentForm); }, }; </script> \ No newline at end of file diff --git a/src/components/feature/FeatureLinkedForm.vue b/src/components/feature/FeatureLinkedForm.vue index 960f63371b388c965bfe623add3ba30b190030e4..80f10d776a4754d01cdcfb0d1568a566964d5e8b 100644 --- a/src/components/feature/FeatureLinkedForm.vue +++ b/src/components/feature/FeatureLinkedForm.vue @@ -10,7 +10,9 @@ <i class="ui times icon"></i> </button> </h4> - {{ form.errors }} + <ul id="errorlist-links" class="errorlist"> + <li v-for="error in form.errors" :key="error" v-html="error"></li> + </ul> <div class="visible-fields"> <div class="two fields"> <div class="required field"> @@ -53,8 +55,10 @@ export default { }, computed: { - featureOptions: function() { - return this.features.map(el=> `${el.title} (${el.display_creator} - ${el.created_on})`) + featureOptions: function () { + return this.features.map( + (el) => `${el.title} (${el.display_creator} - ${el.created_on})` + ); }, selected_relation_type: { // getter @@ -92,7 +96,7 @@ export default { }, html_name: "relation_type", label: "Type de liaison", - value: "", + value: "Doublon", }, feature_to: { errors: null, @@ -118,6 +122,19 @@ export default { feature_to: this.form.feature_to.value, }); }, + checkForm() { + if (this.form.feature_to.value === "") { + this.form.errors = [ + "<strong>Choisir un signalement lié</strong><br/> Pourriez-vous choisir un signalement pour la nouvelle liaison ?", + ]; + document + .getElementById("errorlist-links") + .scrollIntoView({ block: "start", inline: "nearest" }); + return false; + } + this.form.errors = []; + return true; + }, }, }; </script> \ No newline at end of file diff --git a/src/components/feature_type/FeatureTypeCustomForm.vue b/src/components/feature_type/FeatureTypeCustomForm.vue index fa70cbfd986c21ef88b08497b2e583f80e48c683..140a62b0765e06417bb45fb7e03a1283aebac863 100644 --- a/src/components/feature_type/FeatureTypeCustomForm.vue +++ b/src/components/feature_type/FeatureTypeCustomForm.vue @@ -24,7 +24,11 @@ @blur="updateStore" /> <small>{{ form.label.help_text }}</small> - {{ form.label.errors }} + <ul id="errorlist" class="errorlist"> + <li v-for="error in form.label.errors" :key="error"> + {{ error }} + </li> + </ul> </div> <div class="required field"> @@ -39,9 +43,14 @@ @blur="updateStore" /> <small>{{ form.name.help_text }}</small> - {{ form.name.errors }} + <ul id="errorlist" class="errorlist"> + <li v-for="error in form.name.errors" :key="error"> + {{ error }} + </li> + </ul> </div> </div> + <div class="three fields"> <div class="required field"> <label :for="form.position.id_for_label">{{ @@ -58,8 +67,13 @@ /> </div> <small>{{ form.position.help_text }}</small> - {{ form.position.errors }} + <ul id="errorlist" class="errorlist"> + <li v-for="error in form.position.errors" :key="error"> + {{ error }} + </li> + </ul> </div> + <div class="required field"> <label :for="form.field_type.id_for_label">{{ form.field_type.label @@ -70,7 +84,13 @@ :selected="selectedFieldType" :selection.sync="selectedFieldType" /> + <ul id="errorlist" class="errorlist"> + <li v-for="error in form.field_type.errors" :key="error"> + {{ error }} + </li> + </ul> </div> + <div v-if="selectedFieldType === 'Liste de valeurs'" class="field field-list-options required field" @@ -87,7 +107,11 @@ class="options-field" /> <small>{{ form.help_text }}</small> - {{ form.options.errors }} + <ul id="errorlist" class="errorlist"> + <li v-for="error in form.options.errors" :key="error"> + {{ error }} + </li> + </ul> </div> </div> </div> @@ -120,7 +144,7 @@ export default { form: { dataKey: 0, label: { - errors: null, + errors: [], id_for_label: "label", label: "Label", help_text: "Nom en language naturel du champ", @@ -131,7 +155,7 @@ export default { value: null, }, name: { - errors: null, + errors: [], id_for_label: "name", label: "Nom", html_name: "name", @@ -143,7 +167,7 @@ export default { value: null, }, position: { - errors: null, + errors: [], id_for_label: "position", label: "Position", min_value: 0, // ! check if good values (not found) @@ -153,10 +177,10 @@ export default { field: { max_length: 128, // ! check if good values (not found) }, - value: 0, + value: this.customForm.dataKey - 1, }, field_type: { - errors: null, + errors: [], id_for_label: "field_type", label: "Type de champ", html_name: "field_type", @@ -164,10 +188,10 @@ export default { field: { max_length: 50, }, - value: null, //* field to send to the backend + value: "boolean", }, options: { - errors: null, + errors: [], id_for_label: "options", label: "Options", html_name: "options", @@ -246,6 +270,19 @@ export default { // TODO : supprimer les espaces pour chaque option au début et à la fin QUE à la validation return string.replace(/\s*,\s*/gi, ","); }, + checkCustomForm() { + if (this.form.label.value === null) { + this.form.label.errors = ["Veuillez compléter ce champ."]; + return false; + } else if (this.form.name.value === null) { + this.form.name.errors = ["Veuillez compléter ce champ."]; + this.form.label.errors = []; + return false; + } + this.form.label.errors = []; + this.form.name.errors = []; + return true; + }, }, beforeDestroy() { diff --git a/src/services/project-api.js b/src/services/project-api.js index 14019cb19f74c0b76d568761b5008b4bc24692ae..fd46c39241a72d1928e49af49f081730d497da9c 100644 --- a/src/services/project-api.js +++ b/src/services/project-api.js @@ -1,6 +1,14 @@ import axios from 'axios'; import store from '../store' + +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'); + + const baseUrl = store.state.configuration.VUE_APP_DJANGO_API_BASE; const projectAPI = { diff --git a/src/store/index.js b/src/store/index.js index faebe1b6157cbcb4a4d572b2edc5be1135af5d9c..cb00de2006643cd9aeaa14a981a9b12726994edb 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -8,6 +8,12 @@ import feature_type from "./modules/feature_type" import feature from "./modules/feature" import map from "./modules/map" +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'); + Vue.use(Vuex); @@ -30,6 +36,7 @@ export default new Vuex.Store({ map }, state: { + error: null, logged: false, user: false, configuration:null, @@ -43,6 +50,9 @@ export default new Vuex.Store({ }, mutations: { + error(state, data) { + return state.error = data + }, SET_PROJECTS(state, projects) { state.projects = projects; }, @@ -125,15 +135,23 @@ export default new Vuex.Store({ password: payload.password, }) .then((response) => { + commit('error', null) if (response && response.status === 200) { // * use stored previous route to go back after login if page not open on login at first - const routerHistory = router.options.routerHistory[0].name !== "login" ? router.options.routerHistory : "/" + let routerHistory = '' + if (router.options.routerHistory[0] != undefined){ + routerHistory = router.options.routerHistory[0].name !== "login" ? router.options.routerHistory : "/" + } else { + routerHistory = "/" + } commit("SET_USER", response.data.user); router.push(routerHistory[routerHistory.length - 1] || "/") dispatch("GET_USER_LEVEL_PROJECTS"); } }) - .catch(() => { + .catch((error) => { + if (error.response.status === 403) + commit('error', error.response.data.detail) commit("SET_USER", false); }); } diff --git a/src/store/modules/feature.js b/src/store/modules/feature.js index 07c33eef2583583f5aa911af5b58dc63bef7d752..f6f7f42e364c7e2ba603110d8f9ee261e068074c 100644 --- a/src/store/modules/feature.js +++ b/src/store/modules/feature.js @@ -1,5 +1,11 @@ const axios = require("axios"); -//import router from '../../router' +import router from '../../router' + +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'); const feature = { @@ -27,8 +33,8 @@ const feature = { SET_EXTRA_FORM(state, extra_form) { state.extra_form = extra_form; }, - ADD_ATTACHMENT_FORM(state, dataKey) { - state.attachmentFormset = [...state.attachmentFormset, { dataKey }]; + ADD_ATTACHMENT_FORM(state, attachmentFormset) { + state.attachmentFormset = [...state.attachmentFormset, attachmentFormset]; }, UPDATE_ATTACHMENT_FORM(state, payload) { const index = state.attachmentFormset.findIndex((el) => el.dataKey === payload.dataKey); @@ -37,6 +43,9 @@ const feature = { REMOVE_ATTACHMENT_FORM(state, payload) { state.attachmentFormset = state.attachmentFormset.filter(form => form.dataKey !== payload); }, + CLEAR_ATTACHMENT_FORM(state) { + state.attachmentFormset = []; + }, ADD_LINKED_FORM(state, dataKey) { state.linkedFormset = [...state.linkedFormset, { dataKey }]; }, @@ -55,19 +64,21 @@ const feature = { axios .get(`${this.state.configuration.VUE_APP_DJANGO_API_BASE}projects/${project_slug}/feature/`) .then((response) => { - const features = response.data.features; - commit("SET_FEATURES", features); - //dispatch("map/ADD_FEATURES", null, { root: true }); //todo: should check if map was initiated + if (response.status === 200 && response.data) { + const features = response.data.features; + commit("SET_FEATURES", features); + //dispatch("map/ADD_FEATURES", null, { root: true }); //todo: should check if map was initiated + } }) .catch((error) => { throw error; }); }, - POST_FEATURE({ state, rootState }, routeName) { - let extraFormOject = {}; + SEND_FEATURE({ state, rootState, dispatch }, routeName) { + let extraFormObject = {}; //* prepare an object to be flatten in properties of geojson for (const field of state.extra_form) { - extraFormOject[field.name] = field.value; + extraFormObject[field.name] = field.value; } const geojson = { "id": state.form.feature_id, @@ -79,7 +90,7 @@ const feature = { "status": state.form.status.value, "project": rootState.project_slug, "feature_type": rootState.feature_type.current_feature_type_slug, - ...extraFormOject + ...extraFormObject } } @@ -87,37 +98,65 @@ const feature = { axios .put(`${this.state.configuration.VUE_APP_DJANGO_API_BASE}features/${state.form.feature_id}/`, geojson) .then((response) => { - console.log(response, response.data) + if (response.status === 200 && response.data) { + router.push({ + name: "project_detail", + params: { + slug: rootState.project_slug, + message: "Le signalement a été mis à jour", + }, + }); + } }) .catch((error) => { throw error; }); - } else { - axios + } else { + axios .post(`${this.state.configuration.VUE_APP_DJANGO_API_BASE}features/`, geojson) .then((response) => { - console.log(response, response.data) + if (response.status === 201 && response.data) { + dispatch("SEND_ATTACHMENTS", response.data.id) + router.push({ + name: "project_detail", + params: { + slug: rootState.project_slug, + message: "Le signalement a été crée", + }, + }); + } }) .catch((error) => { throw error; }); } }, + + SEND_ATTACHMENTS({ state }, featureId) { + for (let attacht of state.attachmentFormset) { + let formdata = new FormData(); + formdata.append("file", attacht.fileToImport, attacht.fileToImport.name); + const data = { + title: attacht.title, + info: attacht.info, + } + formdata.append("data", JSON.stringify(data)); + axios + .post(`${this.state.configuration.VUE_APP_DJANGO_API_BASE}features/${featureId}/attachments/`, formdata) + .then((response) => { + if (response.status === 200 && response.data) { + console.log(response, response.data) + return "La pièce jointe a bien été ajouté" + } + }) + .catch((error) => { + throw error; + }); + } + } //DELETE_FEATURE({ state }, feature_slug) { //console.log("Deleting feature:", feature_slug, state) - /* axios - .post(`${DJANGO_API_BASE}feature_type/`, data) - .then((response) => { - const routerHistory = router.options.routerHistory - commit("SET_USER", response.data.user); - router.push(routerHistory[routerHistory.length - 1] || "/") - dispatch("GET_USER_LEVEL_PROJECTS"); - }) - .catch(() => { - commit("SET_USER", false) - }); */ - // }, // POST_COMMENT({ state }, data) { //console.log("post comment", data, state) diff --git a/src/store/modules/feature_type.js b/src/store/modules/feature_type.js index b1c65d094d405450babbad5a0a782fca292f318f..f4148c1783d7d4c6ecc97ea7186b0bb418d12296 100644 --- a/src/store/modules/feature_type.js +++ b/src/store/modules/feature_type.js @@ -1,5 +1,10 @@ import axios from "axios" +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'); const feature_type = { @@ -77,6 +82,7 @@ const feature_type = { async SEND_FEATURE_TYPE({ state, getters, rootGetters }, requestType) { const data = { 'title': state.form.title.value, + 'title_optional': state.form.title_optional.value, 'geom_type': state.form.geom_type.value, 'color': state.form.color.value, 'colors_style': state.form.colors_style.value, @@ -122,7 +128,7 @@ const feature_type = { } }, - POST_FEATURES_FROM_GEOJSON({ state, dispatch }, payload) { + SEND_FEATURES_FROM_GEOJSON({ state, dispatch }, payload) { const { feature_type_slug } = payload if (state.fileToImport.size > 0) { diff --git a/src/store/modules/map.js b/src/store/modules/map.js index 207d3ebc9696cd3741160b7165027a6d9d9573c1..f29ef9f4d128729e027c6364005edcad240fce6c 100644 --- a/src/store/modules/map.js +++ b/src/store/modules/map.js @@ -1,6 +1,12 @@ const axios = require("axios"); import { mapUtil } from "@/assets/js/map-util.js"; +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'); + const map = { namespaced: true, diff --git a/src/views/feature/Feature_detail.vue b/src/views/feature/Feature_detail.vue index b894f3b31c48bb6c94459cddffe805fae4eadadf..d6743cf7508fad89316f35932fc8ae655fb66f6f 100644 --- a/src/views/feature/Feature_detail.vue +++ b/src/views/feature/Feature_detail.vue @@ -176,25 +176,26 @@ <div class="seven wide column"> <h2 class="ui header">Pièces jointes</h2> - <div v-for="pj in attachments" :key="pj.title" class="ui divided items"> + <div v-for="pj in attachments" :key="pj.id" class="ui divided items"> <div class="item"> <a class="ui tiny image" target="_blank" - :href="pj.attachment_file.url" + :href="DJANGO_BASE_URL + pj.attachment_file" > <img - v-if="pj.extension === '.pdf'" - src="{% static 'geocontrib/img/pdf.png' %}" + :src=" + pj.extension === '.pdf' + ? require('@/assets/img/pdf.png') + : DJANGO_BASE_URL + pj.attachment_file + " /> - <!-- // ? que faire ? --> - <img v-else :src="pj.attachment_file.url" /> </a> <div class="middle aligned content"> <a class="header" target="_blank" - :href="pj.attachment_file.url" + :href="DJANGO_BASE_URL + pj.attachment_file" >{{ pj.title }}</a > <div class="description"> @@ -377,6 +378,7 @@ import frag from "vue-frag"; import { mapState } from "vuex"; import { mapUtil } from "@/assets/js/map-util.js"; +import featureAPI from "@/services/feature-api"; const axios = require("axios"); export default { @@ -402,18 +404,8 @@ export default { }, }, */ ], - attachments: [ - // TODO : Récupérer depuis l'api - /* { - attachment_file: { - url: "http://localhost:8000/media/user_1/albinoscom.jpg", - }, - extension: "jpg", - title: "albinos", - info: "Drôle de bête", - }, */ - ], - // TODO : Récupérer depuis l'api + attachments: [], + // TODO : Récupérer events depuis l'api events: [], comment_form: { attachment_file: { @@ -564,6 +556,9 @@ export default { "feature_type/SET_CURRENT_FEATURE_TYPE_SLUG", this.$route.params.slug_type_signal ); + featureAPI + .getFeatureAttachments(this.$route.params.slug_signal) + .then((data) => (this.attachments = data)); }, mounted() { diff --git a/src/views/feature/Feature_edit.vue b/src/views/feature/Feature_edit.vue index aa803f96591f2cdddc4758b2485a18985b6652c1..2b60ca875aa7c1750313258fa4296d3ad3cbd172 100644 --- a/src/views/feature/Feature_edit.vue +++ b/src/views/feature/Feature_edit.vue @@ -19,7 +19,7 @@ > <!-- Feature Fields --> <div class="two fields"> - <div class="required field"> + <div :class="field_title"> <label :for="form.title.id_for_label">{{ form.title.label }}</label> <input type="text" @@ -30,7 +30,11 @@ v-model="form.title.value" @blur="updateStore" /> - {{ form.title.errors }} + <ul id="errorlist-title" class="errorlist"> + <li v-for="error in form.title.errors" :key="error"> + {{ error }} + </li> + </ul> </div> <div class="required field"> <label :for="form.status.id_for_label">{{ @@ -41,8 +45,6 @@ :selected="selected_status.name" :selection.sync="selected_status" /> - - {{ form.status.errors }} </div> </div> <div class="field"> @@ -55,7 +57,6 @@ v-model="form.description.value" @blur="updateStore" ></textarea> - {{ form.description.errors }} </div> <!-- Geom Field --> @@ -65,7 +66,8 @@ <!-- Import GeoImage --> <div v-frag v-if="feature_type && feature_type.geom_type === 'point'"> <p> - <button @click="showGeoRef=true;" + <button + @click="showGeoRef = true" id="add-geo-image" type="button" class="ui compact button" @@ -75,25 +77,44 @@ Vous pouvez utiliser une image géoréférencée pour localiser le signalement. </p> - <div v-if="showGeoRef"> - <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"> - <label>Image (png ou jpeg)</label> - <label class="ui icon button" for="image_file"> - <i class="file icon"></i> - <span class="label">Sélectionner une image ...</span> - </label> - <input type="file" accept="image/jpeg, image/png" style="display:none;" ref="file" v-on:change="handleFileUpload()" - name="image_file" class="image_file" id="image_file" > - <p class="error-message" style="color:red;">{{ erreurUploadMessage }}</p> - </div> - <button @click="georeferencement()" id="get-geom-from-image-file" type='button' class="ui positive right labeled icon button"> - Importer - <i class="checkmark icon"></i> - </button> + <div v-if="showGeoRef"> + <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"> + <label>Image (png ou jpeg)</label> + <label class="ui icon button" for="image_file"> + <i class="file icon"></i> + <span class="label">Sélectionner une image ...</span> + </label> + <input + type="file" + accept="image/jpeg, image/png" + style="display: none" + ref="file" + v-on:change="handleFileUpload()" + name="image_file" + class="image_file" + id="image_file" + /> + <p class="error-message" style="color: red"> + {{ erreurUploadMessage }} + </p> + </div> + <button + @click="georeferencement()" + id="get-geom-from-image-file" + type="button" + class="ui positive right labeled icon button" + > + Importer + <i class="checkmark icon"></i> + </button> </div> <p v-if="showGeoPositionBtn"> - <button @click="create_point_geoposition()" + <button + @click="create_point_geoposition()" id="create-point-geoposition" type="button" class="ui compact button" @@ -102,19 +123,27 @@ signalement à partir de votre géolocalisation </button> </p> - <span id="erreur-geolocalisation" v-if="erreurGeolocalisationMessage"> + <span + id="erreur-geolocalisation" + 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> + <p id="erreur-geolocalisation-message"> + {{ erreurGeolocalisationMessage }} + </p> </div> <br /> </span> </div> - - {{ form.geom.errors }} + <ul id="errorlist-geom" class="errorlist"> + <li v-for="error in form.geom.errors" :key="error"> + {{ error }} + </li> + </ul> <!-- Map --> <input type="hidden" @@ -130,7 +159,7 @@ "geocontrib/map-layers/sidebar-layers.html" with basemaps=serialized_base_maps layers=serialized_layers project=project.slug%} {% endif %} --> - <SidebarLayers v-if="baseMaps && map" /> + <SidebarLayers v-if="basemaps && map" /> </div> </div> @@ -147,14 +176,12 @@ <!-- Pièces jointes --> <div class="ui horizontal divider">PIÈCES JOINTES</div> - <!-- {{ attachment_formset.non_form_errors }} --> - <div id="formsets-attachment"> - <!-- {{ attachment_formset.management_form }} --> <FeatureAttachmentForm v-for="form in attachmentFormset" :key="form.dataKey" :attachmentForm="form" + ref="attachementForm" /> </div> @@ -169,15 +196,13 @@ <!-- Signalements liés --> <div class="ui horizontal divider">SIGNALEMENTS LIÉS</div> - <!-- {{ linked_formset.non_form_errors }} --> <div id="formsets-link"> - <!-- {{ linked_formset.management_form }} --> - <FeatureLinkedForm v-for="form in linkedFormset" :key="form.dataKey" :linkedForm="form" :features="features" + ref="linkedForm" /> </div> <button @@ -207,6 +232,7 @@ 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"; @@ -214,6 +240,13 @@ import { mapUtil } from "@/assets/js/map-util.js"; const axios = require("axios"); import flip from "@turf/flip"; +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'); + + export default { name: "Feature_edit", @@ -231,12 +264,12 @@ export default { data() { return { - map:null, - file:null, - showGeoRef:false, - showGeoPositionBtn:true, - erreurGeolocalisationMessage:null, - erreurUploadMessage:null, + map: null, + file: null, + showGeoRef: false, + showGeoPositionBtn: true, + erreurGeolocalisationMessage: null, + erreurUploadMessage: null, attachmentDataKey: 0, linkedDataKey: 0, statusChoices: [ @@ -249,7 +282,7 @@ export default { ], form: { title: { - errors: null, + errors: [], id_for_label: "name", field: { max_length: 30, @@ -259,20 +292,23 @@ export default { value: "", }, status: { - errors: null, id_for_label: "status", html_name: "status", label: "Statut", - value: "Brouillon", + value: { + value: "draft", + name: "Brouillon", + }, }, description: { - errors: null, + errors: [], id_for_label: "description", html_name: "description", label: "Description", value: "", }, geom: { + errors: [], label: "Localisation", value: null, }, @@ -282,6 +318,7 @@ export default { computed: { ...mapState(["project"]), + ...mapState("map", ["basemaps"]), ...mapState("feature", [ "attachmentFormset", "linkedFormset", @@ -290,6 +327,15 @@ export default { ]), ...mapGetters("feature_type", ["feature_type"]), + field_title() { + if (this.feature_type) { + if (this.feature_type.title_optional) { + return "field"; + } + } + return "required field"; + }, + currentRouteName() { return this.$route.name; }, @@ -328,15 +374,15 @@ export default { methods: { initForm() { if (this.currentRouteName === "editer-signalement") { - for (let el in this.feature) { - if (el && this.form[el]) { - if (el === "status") { - const value = this.feature[el]; - this.form[el].value = this.statusChoices.find( - (el) => el.value === value + for (let key in this.feature) { + if (key && this.form[key]) { + if (key === "status") { + const value = this.feature[key]; + this.form[key].value = this.statusChoices.find( + (key) => key.value === value ); } else { - this.form[el].value = this.feature[el]; + this.form[key].value = this.feature[key]; } } } @@ -344,51 +390,52 @@ export default { } }, create_point_geoposition() { - function success(position) { - const latitude = position.coords.latitude - const longitude = position.coords.longitude + const latitude = position.coords.latitude; + const longitude = position.coords.longitude; - var layer = L.circleMarker([latitude, longitude]) - this.add_layer_call_back(layer) + var layer = L.circleMarker([latitude, longitude]); + this.add_layer_call_back(layer); this.map.setView([latitude, longitude]); } function error(err) { - this.erreurGeolocalisationMessage=err.message; - + this.erreurGeolocalisationMessage = err.message; } - this.erreurGeolocalisationMessage=null; + this.erreurGeolocalisationMessage = null; if (!navigator.geolocation) { - this.erreurGeolocalisationMessage="La géolocalisation n'est pas supportée par votre navigateur."; + this.erreurGeolocalisationMessage = + "La géolocalisation n'est pas supportée par votre navigateur."; } else { - navigator.geolocation.getCurrentPosition(success.bind(this), error.bind(this)); + navigator.geolocation.getCurrentPosition( + success.bind(this), + error.bind(this) + ); } - }, handleFileUpload() { this.file = this.$refs.file.files[0]; - console.log('>>>> 1st element in files array >>>> ', this.file); + console.log(">>>> 1st element in files array >>>> ", this.file); }, - georeferencement(){ + georeferencement() { console.log("georeferencement"); const url = `${this.$store.state.configuration.VUE_APP_DJANGO_API_BASE}exif-geom-reader/`; let formData = new FormData(); - formData.append('file', this.file); - console.log('>> formData >> ', formData); - let self=this; - axios.post(url, - formData, { - headers: { - 'Content-Type': 'multipart/form-data' - } - } - ).then(function () { - console.log('SUCCESS!!'); + formData.append("file", this.file); + console.log(">> formData >> ", formData); + let self = this; + axios + .post(url, formData, { + headers: { + "Content-Type": "multipart/form-data", + }, + }) + .then(function () { + console.log("SUCCESS!!"); }) .catch(function () { - console.log('FAILURE!!'); - self.erreurUploadMessage='FAILURE!!'; + console.log("FAILURE!!"); + self.erreurUploadMessage = "FAILURE!!"; }); }, @@ -409,10 +456,26 @@ export default { }, add_attachement_formset() { - this.$store.commit("feature/ADD_ATTACHMENT_FORM", this.attachmentDataKey); // * create an object with the counter in store + this.$store.commit("feature/ADD_ATTACHMENT_FORM", { + dataKey: this.attachmentDataKey, + }); // * create an object with the counter in store this.attachmentDataKey += 1; // * increment counter for key in v-for }, + addExistingAttachementFormset(attachementFormset) { + for (const attachment of attachementFormset) { + console.log("attachment", attachment); + this.$store.commit("feature/ADD_ATTACHMENT_FORM", { + dataKey: this.attachmentDataKey, + title: attachment.title, + attachment_file: attachment.attachment_file, + info: attachment.info, + id: attachment.id, + }); + this.attachmentDataKey += 1; + } + }, + add_linked_formset() { this.$store.commit("feature/ADD_LINKED_FORM", this.linkedDataKey); // * create an object with the counter in store this.linkedDataKey += 1; // * increment counter for key in v-for @@ -428,22 +491,89 @@ export default { }); }, - postForm() { + checkFormTitle() { if (this.form.title.value) { - this.form.title.errors = null; - this.$store.dispatch("feature/POST_FEATURE", this.currentRouteName); + this.form.title.errors = []; + return true; + } else if ( + !this.form.title.errors.includes("Veuillez compléter ce champ.") + ) { + this.form.title.errors.push("Veuillez compléter ce champ."); + document + .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."); + document + .getElementById("errorlist-geom") + .scrollIntoView({ block: "end", inline: "nearest" }); + } + return false; + }, + + 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; + }, + + goBackToProject(message) { + this.$router.push({ + name: "project_detail", + params: { + slug: this.$store.state.project_slug, + message, + }, + }); + }, + + postForm() { + let is_valid = true; + if (!this.feature_type.title_optional) { + is_valid = + this.checkFormTitle() && + this.checkFormGeom() && + this.checkAddedForm(); } else { - this.form.title.errors = "Veuillez compléter ce champ."; + is_valid = this.checkFormGeom() && this.checkAddedForm(); + } + + if (is_valid) { + this.$store.dispatch("feature/SEND_FEATURE", this.currentRouteName); } }, + //* ************* MAP *************** *// + onFeatureTypeLoaded() { var geomLeaflet = { point: "circlemarker", linestring: "polyline", polygon: "polygon", }; - // console.log(this.feature_type) var geomType = this.feature_type.geom_type; var drawConfig = { polygon: false, @@ -614,8 +744,8 @@ export default { this.drawControlFull.addTo(this.map); this.updateGeomField(""); if (geomType === "point") { - this.showGeoPositionBtn=true; - this.erreurGeolocalisationMessage=""; + this.showGeoPositionBtn = true; + this.erreurGeolocalisationMessage = ""; } }.bind(this) ); @@ -687,15 +817,11 @@ export default { throw error; }); - - - document.addEventListener("change-layers-order", (event) => { // Reverse is done because the first layer in order has to be added in the map in last. // Slice is done because reverse() changes the original array, so we make a copy first mapUtil.updateOrder(event.detail.layers.slice().reverse()); }); - }, add_layer_call_back(layer) { @@ -706,8 +832,8 @@ export default { //this.updateGeomField(wellknown.stringify(layer.toGeoJSON())) this.updateGeomField(layer.toGeoJSON()); if (this.feature_type.geomType === "point") { - this.showGeoPositionBtn=false; - this.erreurGeolocalisationMessage=""; + this.showGeoPositionBtn = false; + this.erreurGeolocalisationMessage = ""; } }, }, @@ -720,6 +846,16 @@ export default { "feature_type/SET_CURRENT_FEATURE_TYPE_SLUG", this.$route.params.slug_type_signal ); + + // todo : mutualize in store with feature_detail.vue + if (this.$route.params.slug_signal) { + featureAPI + .getFeatureAttachments(this.$route.params.slug_signal) + .then((data) => this.addExistingAttachementFormset(data)); + } else { + //* be sure that previous attachemntFormset has been cleared for creation + this.$store.commit("feature/CLEAR_ATTACHMENT_FORM"); + } }, mounted() { @@ -727,8 +863,6 @@ export default { this.initMap(); }, }; - -// TODO : add script from django and convert: </script> <style> diff --git a/src/views/feature/Feature_list.vue b/src/views/feature/Feature_list.vue index 6c347c7620e0ca65e43b352dd5c0b178f29cd8f8..8a2764cc2e69f5e25edc37309d966c198239711d 100644 --- a/src/views/feature/Feature_list.vue +++ b/src/views/feature/Feature_list.vue @@ -69,6 +69,29 @@ </div> </div> </div> + + <div v-if="project && feature_types" class="item right"> + <div + + v-if="checkedFeatures.length" + class=" + ui + top + center + pointing + compact + button button-hover-red + " + data-tooltip="Effacer tous les types de signalements sélectionnés" + data-position="left center" + data-variation="mini" + > + <i + class="grey trash icon" + @click="modalAllDelete()" + ></i> + </div> + </div> </div> </div> </div> @@ -127,6 +150,10 @@ <table id="table-features" class="ui compact table"> <thead> <tr> + <th class="center"> + + </th> + <th class="center">Statut <i :class="{ down: isSortedAsc('statut'),up:isSortedDesc('statut') }" class="icon sort" @click="changeSort('statut')"/></th> <th class="center">Type <i :class="{ down: isSortedAsc('type'),up:isSortedDesc('type') }" class="icon sort" @click="changeSort('type')"/></th> <th class="center">Nom <i :class="{ down: isSortedAsc('nom'),up:isSortedDesc('nom') }" class="icon sort" @click="changeSort('nom')"/></th> @@ -139,6 +166,19 @@ v-for="(feature, index) in getPaginatedFeatures()" :key="index" > + <td class="center"> + <div class="ui checkbox"> + <input + type="checkbox" + :id="feature.id" + :value="feature.id" + v-model="checkedFeatures" + :checked="checkedFeatures[feature.id]" + > + <label></label> + </div> + </td> + <td class="center"> <div v-if="feature.properties.status.value == 'archived'" data-tooltip="Archivé"> <i class="grey archive icon"></i> @@ -242,6 +282,41 @@ </div> </div> + <!-- MODAL ALL DELETE FEATURE TYPE --> + <div + v-if="modalAllDeleteOpen" + class="ui dimmer modals page transition visible active" + style="display: flex !important" + > + <div + :class="[ + 'ui mini modal subscription', + { 'active visible': modalAllDeleteOpen }, + ]" + > + <i @click="modalAllDeleteOpen = false" class="close icon"></i> + <div class="ui icon header"> + <i class="trash alternate icon"></i> + Êtes-vous sûr de vouloir effacer + <span v-if="checkedFeatures.length == 1"> + un signalement ? + </span> + <span v-else> + ces {{checkedFeatures.length}} signalements ? + </span> + </div> + <div class="actions"> + + <button + @click="deleteAllFeatureSelection()" + type="button" + class="ui red compact fluid button" + > + Confirmer la suppression + </button> + </div> + </div> + </div> </div> </template> @@ -264,6 +339,8 @@ export default { data() { return { + modalAllDeleteOpen: false, + checkedFeatures: [], form: { type: { selected: null, @@ -329,6 +406,31 @@ export default { }, methods: { + modalAllDelete(){ + return (this.modalAllDeleteOpen = !this.modalAllDeleteOpen); + }, + deleteFeature(feature){ + const url=`${this.$store.state.configuration.VUE_APP_DJANGO_API_BASE}features/${feature.feature_id}`; + axios + .delete(url, { + }) + .then(() => { + if(!this.modalAllDeleteOpen){ + this.$router.go(); + } + }) + .catch(() => { + return false; + }); + }, + deleteAllFeatureSelection(){ + let feature = {} + this.checkedFeatures.forEach(feature_id => { + feature = {'feature_id': feature_id}; + this.deleteFeature(feature); + }); + this.modalAllDelete(); + }, getFeatureDisplayName(feature){ return feature.properties.title || feature.id; }, @@ -372,7 +474,7 @@ export default { }) } - + return filterdFeatures.slice( this.pagination.start, this.pagination.end diff --git a/src/views/feature_type/Feature_type_detail.vue b/src/views/feature_type/Feature_type_detail.vue index a595e6ea9760b71202e6fd9ad3ad5227858cc54f..cd18f5ec2fa0f784d0cf5452bbaef8081649ba32 100644 --- a/src/views/feature_type/Feature_type_detail.vue +++ b/src/views/feature_type/Feature_type_detail.vue @@ -242,7 +242,7 @@ export default { }, importGeoJson() { - this.$store.dispatch("feature_type/POST_FEATURES_FROM_GEOJSON", { + this.$store.dispatch("feature_type/SEND_FEATURES_FROM_GEOJSON", { slug: this.$route.params.slug, feature_type_slug: this.$route.params.feature_type_slug, fileToImport: this.fileToImport, diff --git a/src/views/feature_type/Feature_type_edit.vue b/src/views/feature_type/Feature_type_edit.vue index 7b198c7f03a260048a4fbe23e2d451ec315cc14b..e3ccfb670d45e981eff5795891acf54a13d70cb1 100644 --- a/src/views/feature_type/Feature_type_edit.vue +++ b/src/views/feature_type/Feature_type_edit.vue @@ -25,6 +25,7 @@ <p v-if="action === 'create'"> Ces champs par défaut existent pour tous les types de signalement: </p> + <div class="two fields"> <div class="required field"> <label :for="form.title.id_for_label">{{ form.title.label }}</label> @@ -43,6 +44,7 @@ </li> </ul> </div> + <div class="required field"> <label :for="form.geom_type.id_for_label">{{ form.geom_type.label @@ -69,6 +71,16 @@ <!-- {{ form.color.errors }} --> </div> </div> + <div class="field"> + <div class="ui checkbox"> + <input + :name="form.title_optional.html_name" + v-model="form.title_optional.value" + type="checkbox" + /> + <label>{{form.title_optional.label}}</label> + </div> + </div> <!-- //* s'affiche après sélection d'option de type liste dans type de champ --> <div @@ -109,6 +121,7 @@ :key="form.dataKey" :dataKey="form.dataKey" :customForm="form" + ref="customForms" /> </div> @@ -204,6 +217,13 @@ export default { html_name: "title", value: null, }, + title_optional: { + errors: null, + id_for_label: "title_optional", + html_name: "title_optional", + label: "Titre du signalement optionnel", + value: false, + }, geom_type: { id_for_label: "geom_type", label: "Type de géométrie", @@ -211,7 +231,7 @@ export default { max_length: 128, // ! Vérifier la valeur dans django }, html_name: "geom_type", - value: "Point", + value: "point", }, }, reservedKeywords: [ @@ -305,7 +325,7 @@ export default { // * find feature_type and fill form values if (this.form[el]) this.form[el].value = formData[el]; } - //! add custom fields using ONLY this function, incrementing dataKey for Vue updating correctly components + //! add custom fields using ONLY this function, incrementing dataKey for Vue to correctly update components formData.customfield_set.forEach((el) => this.addCustomForm(el)); this.updateStore(); }, @@ -319,17 +339,28 @@ export default { this.$store.commit("feature_type/UPDATE_FORM", { color: this.form.color, title: this.form.title, + title_optional: this.form.title_optional, geom_type: this.form.geom_type, colors_style: this.form.colors_style, }); }, - checkForm() { + checkCustomForms() { + if (this.$refs.customForms) + for (const customForm of this.$refs.customForms) { + if (customForm.checkCustomForm() === false) { + return false; + } + } + return true; //* fallback if all customForms returned true + }, + + checkForms() { if (this.form.title.value) { this.form.title.errors = []; - return true; + return this.checkCustomForms(); //* if customForms are ok, validate, if get out function } else if ( - !this.form.title.errors.includes("Veuillez compléter ce champ.") // TODO : Gérer les autres champs + !this.form.title.errors.includes("Veuillez compléter ce champ.") ) { this.form.title.errors.push("Veuillez compléter ce champ."); document @@ -352,7 +383,7 @@ export default { sendFeatureType() { // * si édition d'une feature_type déja existante, faire un put const requestType = this.action === "edit" ? "put" : "post"; - if (this.checkForm()) { + if (this.checkForms()) { this.$store .dispatch("feature_type/SEND_FEATURE_TYPE", requestType) .then(({ status }) => { @@ -371,7 +402,7 @@ export default { postFeatures(feature_type_slug) { this.$store - .dispatch("feature_type/POST_FEATURES_FROM_GEOJSON", { + .dispatch("feature_type/SEND_FEATURES_FROM_GEOJSON", { slug: this.$route.params.slug, feature_type_slug, }) @@ -389,7 +420,7 @@ export default { async postFeatureTypeThenFeatures() { const requestType = this.action === "edit" ? "put" : "post"; - if (this.checkForm()) { + if (this.checkForms()) { await this.$store .dispatch("feature_type/SEND_FEATURE_TYPE", requestType) .then(({ feature_type_slug }) => { @@ -489,10 +520,10 @@ export default { } if (this.action === "duplicate") { //* replace original name with new default title - this.form.title.value += ` (Copie ${new Date() + this.form.title.value += ` (Copie-${new Date() .toLocaleString() .slice(0, -3) - .replace(",", "")} )`; + .replace(",", "")})`; this.updateStore(); // * initialize form in store in case this.form would not be modified } } diff --git a/src/views/project/Project_detail.vue b/src/views/project/Project_detail.vue index 4e8055af6c185ec594437934e0089ab258fb1bba..8bff78d9e0d9dbb71fe8672f0eeb4899fb0d4414 100644 --- a/src/views/project/Project_detail.vue +++ b/src/views/project/Project_detail.vue @@ -74,7 +74,6 @@ <div class="row"> <div class="seven wide column"> - <h3 class="ui header">Types de signalements</h3> <!-- // todo : Create endpoints for feature_types --> <div class="ui middle aligned divided list"> <div @@ -83,7 +82,7 @@ class="item" > <div class="middle aligned content"> - <router-link + <router-link :to="{ name: 'details-type-signalement', params: { feature_type_slug: type.slug }, @@ -429,6 +428,7 @@ </div> </div> </div> + </div> </template> @@ -495,7 +495,6 @@ export default { refreshId() { return "?ver=" + Math.random(); }, - toNewFeatureType() { this.$router.push({ name: "ajouter-type-signalement", @@ -534,7 +533,6 @@ export default { .then((data) => (this.is_suscriber = data.is_suscriber)); }, }, - created() { this.$store.dispatch("GET_PROJECT_INFO", this.slug); projectAPI diff --git a/src/views/project/Project_edit.vue b/src/views/project/Project_edit.vue index c2b1cbdd1cefa4ff350b0b177ed8315d0a753e05..72a6344c09054bf111b1fd2f44b4a4fe9b5e596c 100644 --- a/src/views/project/Project_edit.vue +++ b/src/views/project/Project_edit.vue @@ -13,8 +13,6 @@ <div class="two fields"> <div class="required field"> <label for="title">Titre</label> - <!-- <small>{{ form.title.help_text }}</small - > --><!-- | safe // ? utile ? --> <input type="text" required @@ -23,7 +21,11 @@ id="title" v-model="form.title" /> - <!-- {{ form.title.errors }} --> + <ul id="errorlist-title" class="errorlist"> + <li v-for="error in errors.title" :key="error"> + {{ error }} + </li> + </ul> </div> <div class="field"> <label>Illustration du projet</label> @@ -73,6 +75,7 @@ <input type="number" min="0" + oninput="validity.valid||(value='');" style="padding: 1px 2px" name="archive_feature" id="archive_feature" @@ -80,7 +83,6 @@ /> <div class="ui label">jour(s)</div> </div> - <!-- {{ form.archive_feature.errors }} --> </div> <div class="field"> <label for="delete_feature">Délai avant suppression</label> @@ -88,6 +90,7 @@ <input type="number" min="0" + oninput="validity.valid||(value='');" style="padding: 1px 2px" name="delete_feature" id="delete_feature" @@ -95,7 +98,6 @@ /> <div class="ui label">jour(s)</div> </div> - <!-- {{ form.delete_feature.errors }} --> </div> <div class="required field"> <label for="access_level_pub_feature" @@ -106,6 +108,11 @@ :selected="form.access_level_pub_feature.name" :selection.sync="form.access_level_pub_feature" /> + <ul id="errorlist-access_level_pub_feature" class="errorlist"> + <li v-for="error in errors.access_level_pub_feature" :key="error"> + {{ error }} + </li> + </ul> </div> <div class="required field"> <label for="access_level_arch_feature"> @@ -116,6 +123,11 @@ :selected="form.access_level_arch_feature.name" :selection.sync="form.access_level_arch_feature" /> + <ul id="errorlist-access_level_arch_feature" class="errorlist"> + <li v-for="error in errors.access_level_arch_feature" :key="error"> + {{ error }} + </li> + </ul> </div> </div> @@ -160,6 +172,13 @@ import Dropdown from "@/components/Dropdown.vue"; import { mapGetters } from "vuex"; +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'); + + export default { name: "Project_edit", @@ -179,6 +198,11 @@ export default { name: "Sélectionner une image ...", size: 0, }, + errors: { + title: [], + access_level_pub_feature: [], + access_level_arch_feature: [], + }, form: { title: "", slug: "", @@ -206,7 +230,7 @@ export default { computed: { ...mapGetters(["project"]), - DJANGO_BASE_URL:function () { + DJANGO_BASE_URL: function () { return this.$store.state.configuration.VUE_APP_DJANGO_BASE; }, }, @@ -222,8 +246,8 @@ export default { } }, truncate(n, len) { - var ext = n.substring(n.lastIndexOf(".") + 1, n.length).toLowerCase(); - var filename = n.replace("." + ext, ""); + let ext = n.substring(n.lastIndexOf(".") + 1, n.length).toLowerCase(); + let filename = n.replace("." + ext, ""); if (filename.length <= len) { return n; } @@ -286,7 +310,27 @@ export default { } }, + checkForm() { + for (const key in this.errors) { + if ((key === "title" && this.form[key]) || this.form[key].value) { + this.errors[key] = []; + } else if (!this.errors[key].length) { + this.errors[key].push( + key === "title" + ? "Veuillez compléter ce champ." + : "Sélectionnez un choix valide. Ce choix ne fait pas partie de ceux disponibles." + ); + document + .getElementById(`errorlist-${key}`) + .scrollIntoView({ block: "end", inline: "nearest" }); + return false; + } + } + return true; + }, + async postForm() { + if (!this.checkForm()) return; // todo: check form //let url = `${configuration.VUE_APP_DJANGO_API_BASE}projects/`; const projectData = { @@ -302,14 +346,18 @@ export default { if (this.action === "create" || this.action === "duplicate") { await axios - .post(`${this.$store.state.configuration.VUE_APP_DJANGO_API_BASE}projects/`, projectData) + .post( + `${this.$store.state.configuration.VUE_APP_DJANGO_API_BASE}projects/`, + projectData + ) .then((response) => { if (response && response.status === 201 && response.data) { //* send thumbnail after feature_type was created - if (this.fileToImport) + if (this.fileToImport.size > 0) { this.postProjectThumbnail(response.data.slug); - } else { - this.goBackNrefresh(response.data.slug); + } else { + this.goBackNrefresh(response.data.slug); + } } }) .catch((error) => { @@ -339,15 +387,23 @@ export default { created() { this.definePageType(); + console.log(this.action); if (this.action === "create") { this.thumbnailFileSrc = require("@/assets/img/default.png"); - } else if (this.action === "edit") { + } else if (this.action === "edit" || this.action === "create_from") { if (!this.project) { this.$store.dispatch("GET_PROJECT_INFO", this.$route.params.slug); } - this.form = this.project; - /* this.form.thumbnail = //* add api base to display image src - configuration.VUE_APP_DJANGO_BASE + this.form.thumbnail; */ + this.form = { ...this.project }; //* create a new object to avoid modifying original one + if (this.action === "create_from") { + this.form.title = + this.project.title + + ` (Copie-${new Date() + .toLocaleString() + .slice(0, -3) + .replace(",", "")})`; + this.form.is_project_type = false; + } //* transform string values to objects for dropdowns display (could be in a computed) this.form.access_level_pub_feature = { name: this.project.access_level_pub_feature, diff --git a/src/views/project/Project_type_list.vue b/src/views/project/Project_type_list.vue index 3909eab7ac7c1f54d274d63744467eda05df91a0..1af8b5d584bdd52d5995c7d18c5824b4da86128d 100644 --- a/src/views/project/Project_type_list.vue +++ b/src/views/project/Project_type_list.vue @@ -21,7 +21,7 @@ :to="{ name: 'project_create_from', params: { - slug: project.title, + slug: project.slug, }, }" >{{ project.title }}</router-link diff --git a/src/views/registration/Login.vue b/src/views/registration/Login.vue index 05d38bd0338eef467e39ed8b6647911635d3684f..470d00f3be470f1e4d7b8e989439019d89ba2b92 100644 --- a/src/views/registration/Login.vue +++ b/src/views/registration/Login.vue @@ -62,6 +62,7 @@ <script> +import { mapState } from "vuex"; export default { name: "Login", @@ -76,6 +77,7 @@ export default { }; }, computed: { + ...mapState(["error"]), LOGO_PATH:function () { return this.$store.state.configuration.VUE_APP_LOGO_PATH; }, @@ -87,11 +89,19 @@ export default { }, }, methods: { - login() { + async login() { this.$store.dispatch("LOGIN", { username: this.username_value, password: this.password_value, - }); + }) + .then(() => { + if (this.error != null){ + this.form.errors = "Les informations d'identification sont incorrectes."; + } + }) + .catch(() => { + this.form.errors = this.error + }); }, },