diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index a2e9a6a15..40d2deab8 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -37,12 +37,16 @@ Nous mettons en production au minimum une fois par semaine (et généralement pl
demarches-simplifiees.fr est **compliqué à héberger**. Parmi les problématiques que nous rencontrons :
-- **Sécurité et confidentialité des données** : par nature, demarches-simplifiees.fr traite une quantité importante de données personnelles. La sécurité de l’infrastructure doit être contrôlée et certifiée pour garantir la confidentialité des données. Cela implique par exemple une démarche de mise en conformité avec le [Référentiel Général de Sécurité](https://www.ssi.gouv.fr/entreprise/reglementation/confiance-numerique/le-referentiel-general-de-securite-rgs/), qui est un processus assez lourd.
+- **Sécurité et confidentialité des données** : par nature, demarches-simplifiees.fr est appelé à traiter des natures de données qui peuvent présenter des caractéristiqus plus ou moins sensibles. La sécurité de l’infrastructure doit être contrôlée et certifiée pour garantir la confidentialité des données. Cela implique par exemple une démarche de mise en conformité avec le [Référentiel Général de Sécurité](https://www.ssi.gouv.fr/entreprise/reglementation/confiance-numerique/le-referentiel-general-de-securite-rgs/), qui est un processus assez lourd.
- C’est également valable pour le stockage des pièces-jointes, qui sont souvent des documents d’identités dont la confidentialité doit être garantie.
+ C’est également valable pour le stockage des pièces-jointes, qui peuvent la aussi présenter des particularités et des sensibilités dont la confidentialité doit être garantie.
- **Utilisation de services externes** : demarches-simplifiees.fr s’interconnecte à de nombreux services externes : des APIs (API Entreprise, API Carto, la Base Adresse Nationale, etc.) – mais aussi des services pour le stockage externe des pièces-jointes, l’analyse anti-virus ou l’envoi des emails. Le fonctionnement de demarches-simplifiees.fr dépend de la disponibilité de ces services externes.
- **Mises à jour** : le schéma de la base de données change régulièrement. Nous codons également des scripts pour harmoniser les anciennes données. Parfois des modifications ponctuelles sont effectuées sur des démarches anciennes, pour les mettre en conformité avec de nouvelles règles métiers. Nous maintenons également les dépendances logicielles utilisées – notamment en réagissant rapidement lorsqu’une faille de sécurité est signalée. Ces mises à jour fréquentes en production sont indispensables au bon fonctionnement de l’outil.
Si vous souhaitez adapter demarches-simplifiees.fr à votre besoin, nous vous recommandons de **proposer vos modifications à la base de code principale** (par exemple en créant une issue) **plutôt que d’héberger une autre instance vous-même**.
Dans le cas où vous envisagez d’héberger une instance de demarches-simplifiees.fr vous-même, nous n'avons malheureusement pas les moyens de vous accompagner, ni d'assurer de support technique concernant votre installation.
+
+Dans le cas où vous envisagez d’héberger une instance de demarches-simplifiees.fr vous-même, nous n'avons malheureusement pas les moyens de vous accompagner, ni d'assurer de support technique concernant votre installation.
+
+Totefois, le ministère des armées a déployé une instance au sein de leur intranet. Nous proposons aux acteurs qui sont interessés de les mettre en relation avec eux afin de disposer d'un retour d'expérience, et bénéficier de leur retour.
diff --git a/app/assets/stylesheets/new_design/flex.scss b/app/assets/stylesheets/new_design/flex.scss
index 317ebf55c..b16b0e243 100644
--- a/app/assets/stylesheets/new_design/flex.scss
+++ b/app/assets/stylesheets/new_design/flex.scss
@@ -32,6 +32,10 @@
&.column {
flex-direction: column;
}
+
+ &.row-reverse {
+ flex-direction: row-reverse;
+ }
}
.flex-grow {
diff --git a/app/controllers/new_administrateur/procedures_controller.rb b/app/controllers/new_administrateur/procedures_controller.rb
index ff4734397..e51e75aa0 100644
--- a/app/controllers/new_administrateur/procedures_controller.rb
+++ b/app/controllers/new_administrateur/procedures_controller.rb
@@ -1,49 +1,13 @@
module NewAdministrateur
class ProceduresController < AdministrateurController
- before_action :retrieve_procedure, only: [:champs, :annotations, :update]
- before_action :procedure_locked?, only: [:champs, :annotations, :update]
-
- TYPE_DE_CHAMP_ATTRIBUTES_BASE = [
- :_destroy,
- :libelle,
- :description,
- :order_place,
- :type_champ,
- :id,
- :mandatory,
- :piece_justificative_template,
- :quartiers_prioritaires,
- :cadastres,
- :parcelles_agricoles,
- drop_down_list_attributes: [:value]
- ]
-
- TYPE_DE_CHAMP_ATTRIBUTES = TYPE_DE_CHAMP_ATTRIBUTES_BASE.dup
- TYPE_DE_CHAMP_ATTRIBUTES << {
- types_de_champ_attributes: TYPE_DE_CHAMP_ATTRIBUTES_BASE
- }
+ before_action :retrieve_procedure, only: [:champs, :annotations]
+ before_action :procedure_locked?, only: [:champs, :annotations]
def apercu
@dossier = procedure_without_control.new_dossier
@tab = apercu_tab
end
- def update
- if @procedure.update(procedure_params)
- flash.now.notice = if params[:procedure][:types_de_champ_attributes].present?
- 'Formulaire mis à jour.'
- elsif params[:procedure][:types_de_champ_private_attributes].present?
- 'Annotations privées mises à jour.'
- else
- 'Démarche enregistrée.'
- end
-
- reset_procedure
- else
- flash.now.alert = @procedure.errors.full_messages
- end
- end
-
private
def apercu_tab
@@ -53,12 +17,5 @@ module NewAdministrateur
def procedure_without_control
Procedure.find(params[:id])
end
-
- def procedure_params
- params.required(:procedure).permit(
- types_de_champ_attributes: TYPE_DE_CHAMP_ATTRIBUTES,
- types_de_champ_private_attributes: TYPE_DE_CHAMP_ATTRIBUTES
- )
- end
end
end
diff --git a/app/controllers/new_administrateur/types_de_champ_controller.rb b/app/controllers/new_administrateur/types_de_champ_controller.rb
new file mode 100644
index 000000000..c4bf50110
--- /dev/null
+++ b/app/controllers/new_administrateur/types_de_champ_controller.rb
@@ -0,0 +1,76 @@
+module NewAdministrateur
+ class TypesDeChampController < AdministrateurController
+ before_action :retrieve_procedure, only: [:create, :update, :destroy]
+ before_action :procedure_locked?, only: [:create, :update, :destroy]
+
+ def create
+ type_de_champ = TypeDeChamp.new(type_de_champ_create_params)
+
+ if type_de_champ.save
+ reset_procedure
+ render json: serialize_type_de_champ(type_de_champ), status: :created
+ else
+ render json: { errors: type_de_champ.errors.full_messages }, status: :unprocessable_entity
+ end
+ end
+
+ def update
+ type_de_champ = TypeDeChamp.where(procedure: @procedure).find(params[:id])
+
+ if type_de_champ.update(type_de_champ_update_params)
+ reset_procedure
+ render json: serialize_type_de_champ(type_de_champ)
+ else
+ render json: { errors: type_de_champ.errors.full_messages }, status: :unprocessable_entity
+ end
+ end
+
+ def destroy
+ type_de_champ = TypeDeChamp.where(procedure: @procedure).find(params[:id])
+
+ type_de_champ.destroy!
+ reset_procedure
+
+ head :no_content
+ end
+
+ private
+
+ def serialize_type_de_champ(type_de_champ)
+ {
+ type_de_champ: type_de_champ.as_json(
+ except: [:created_at, :updated_at, :stable_id, :type, :parent_id, :procedure_id, :private],
+ methods: [:piece_justificative_template_filename, :piece_justificative_template_url, :drop_down_list_value]
+ )
+ }
+ end
+
+ def type_de_champ_create_params
+ params.required(:type_de_champ).permit(:libelle,
+ :description,
+ :order_place,
+ :type_champ,
+ :private,
+ :parent_id,
+ :mandatory,
+ :piece_justificative_template,
+ :quartiers_prioritaires,
+ :cadastres,
+ :parcelles_agricoles,
+ :drop_down_list_value).merge(procedure: @procedure)
+ end
+
+ def type_de_champ_update_params
+ params.required(:type_de_champ).permit(:libelle,
+ :description,
+ :order_place,
+ :type_champ,
+ :mandatory,
+ :piece_justificative_template,
+ :quartiers_prioritaires,
+ :cadastres,
+ :parcelles_agricoles,
+ :drop_down_list_value)
+ end
+ end
+end
diff --git a/app/controllers/new_user/dossiers_controller.rb b/app/controllers/new_user/dossiers_controller.rb
index b848ff8ec..f6871c8f5 100644
--- a/app/controllers/new_user/dossiers_controller.rb
+++ b/app/controllers/new_user/dossiers_controller.rb
@@ -188,7 +188,7 @@ module NewUser
def ask_deletion
dossier = current_user.dossiers.includes(:user, procedure: :administrateur).find(params[:id])
- if !dossier.instruction_commencee?
+ if dossier.can_be_deleted_by_user?
dossier.delete_and_keep_track
flash.notice = 'Votre dossier a bien été supprimé.'
redirect_to dossiers_path
@@ -257,7 +257,7 @@ module NewUser
end
def ensure_dossier_can_be_updated
- if !dossier.can_be_updated_by_the_user?
+ if !dossier.can_be_updated_by_user?
flash.alert = 'Votre dossier ne peut plus être modifié'
redirect_to dossiers_path
end
diff --git a/app/helpers/procedure_helper.rb b/app/helpers/procedure_helper.rb
index fe3395201..3222f0064 100644
--- a/app/helpers/procedure_helper.rb
+++ b/app/helpers/procedure_helper.rb
@@ -39,7 +39,8 @@ module ProcedureHelper
type: "champ",
types_de_champ_options: types_de_champ_options.to_json,
types_de_champ: types_de_champ_as_json(procedure.types_de_champ).to_json,
- direct_uploads_url: rails_direct_uploads_url,
+ save_url: procedure_types_de_champ_path(procedure),
+ direct_upload_url: rails_direct_uploads_url,
drag_icon_url: image_url("icons/drag.svg")
}
end
@@ -49,18 +50,12 @@ module ProcedureHelper
type: "annotation",
types_de_champ_options: types_de_champ_options.to_json,
types_de_champ: types_de_champ_as_json(procedure.types_de_champ_private).to_json,
- direct_uploads_url: rails_direct_uploads_url,
+ save_url: procedure_types_de_champ_path(procedure),
+ direct_upload_url: rails_direct_uploads_url,
drag_icon_url: image_url("icons/drag.svg")
}
end
- def procedure_data(procedure)
- {
- types_de_champ: types_de_champ_as_json(procedure.types_de_champ),
- types_de_champ_private: types_de_champ_as_json(procedure.types_de_champ_private)
- }.to_json
- end
-
private
TOGGLES = {
@@ -79,14 +74,12 @@ module ProcedureHelper
types_de_champ
end
- TYPES_DE_CHAMP_INCLUDE = { drop_down_list: { only: :value } }
TYPES_DE_CHAMP_BASE = {
except: [:created_at, :updated_at, :stable_id, :type, :parent_id, :procedure_id, :private],
- methods: [:piece_justificative_template_filename, :piece_justificative_template_url],
- include: TYPES_DE_CHAMP_INCLUDE
+ methods: [:piece_justificative_template_filename, :piece_justificative_template_url, :drop_down_list_value]
}
TYPES_DE_CHAMP = TYPES_DE_CHAMP_BASE
- .merge(include: TYPES_DE_CHAMP_INCLUDE.merge(types_de_champ: TYPES_DE_CHAMP_BASE))
+ .merge(include: { types_de_champ: TYPES_DE_CHAMP_BASE })
def types_de_champ_as_json(types_de_champ)
types_de_champ.includes(:drop_down_list,
diff --git a/app/javascript/new_design/administrateur/DraggableItem.js b/app/javascript/new_design/administrateur/DraggableItem.js
index 95d284120..ea6c1361b 100644
--- a/app/javascript/new_design/administrateur/DraggableItem.js
+++ b/app/javascript/new_design/administrateur/DraggableItem.js
@@ -1,40 +1,23 @@
+import { getJSON, debounce } from '@utils';
+import { DirectUpload } from 'activestorage';
+
export default {
- props: ['state', 'update', 'index', 'item'],
+ props: ['state', 'index', 'item'],
computed: {
- isDirty() {
- return (
- this.state.version &&
- this.state.unsavedInvalidItems.size > 0 &&
- this.state.unsavedItems.has(this.itemId)
- );
- },
- isInvalid() {
+ isValid() {
if (this.deleted) {
- return false;
+ return true;
}
if (this.libelle) {
- return !this.libelle.trim();
+ return !!this.libelle.trim();
}
- return true;
- },
- itemId() {
- return this.item.id || this.clientId;
- },
- changeLog() {
- return [this.itemId, !this.isInvalid];
+ return false;
},
itemClassName() {
const classNames = [`draggable-item-${this.index}`];
if (this.isHeaderSection) {
classNames.push('type-header-section');
}
- if (this.isDirty) {
- if (this.isInvalid) {
- classNames.push('invalid');
- } else {
- classNames.push('dirty');
- }
- }
return classNames.join(' ');
},
isDropDown() {
@@ -73,6 +56,47 @@ export default {
return 'types_de_champ_attributes';
}
},
+ payload() {
+ const payload = {
+ libelle: this.libelle,
+ type_champ: this.typeChamp,
+ mandatory: this.mandatory,
+ description: this.description,
+ drop_down_list_value: this.dropDownListValue,
+ order_place: this.index
+ };
+ if (this.pieceJustificativeTemplate) {
+ payload.piece_justificative_template = this.pieceJustificativeTemplate;
+ }
+ if (this.state.parentId) {
+ payload.parent_id = this.state.parentId;
+ }
+ if (!this.id && this.state.isAnnotation) {
+ payload.private = true;
+ }
+ Object.assign(payload, this.options);
+ return payload;
+ },
+ saveUrl() {
+ if (this.id) {
+ return `${this.state.saveUrl}/${this.id}`;
+ }
+ return this.state.saveUrl;
+ },
+ savePayload() {
+ if (this.deleted) {
+ return {};
+ }
+ return { type_de_champ: this.payload };
+ },
+ saveMethod() {
+ if (this.deleted) {
+ return 'delete';
+ } else if (this.id) {
+ return 'patch';
+ }
+ return 'post';
+ },
typesDeChamp() {
return this.item.types_de_champ;
},
@@ -85,39 +109,46 @@ export default {
return Object.assign({}, this.state, {
typesDeChamp: this.typesDeChamp,
typesDeChampOptions: this.typesDeChampOptions,
- prefix: `${this.state.prefix}[${this.attribute}][${this.index}]`
+ prefix: `${this.state.prefix}[${this.attribute}][${this.index}]`,
+ parentId: this.id
});
}
},
data() {
return {
+ id: this.item.id,
typeChamp: this.item.type_champ,
libelle: this.item.libelle,
mandatory: this.item.mandatory,
description: this.item.description,
- dropDownList: this.item.drop_down_list && this.item.drop_down_list.value,
+ pieceJustificativeTemplate: null,
+ pieceJustificativeTemplateUrl: this.item.piece_justificative_template_url,
+ pieceJustificativeTemplateFilename: this.item
+ .piece_justificative_template_filename,
+ dropDownListValue: this.item.drop_down_list_value,
deleted: false,
- clientId: `id-${clientIds++}`
+ isSaving: false,
+ isUploading: false,
+ hasChanges: false
};
},
- created() {
- for (let path of PATHS_TO_WATCH) {
- this.$watch(path, () => this.update(this.changeLog));
+ watch: {
+ index() {
+ this.update();
}
},
- mounted() {
- if (this.isInvalid) {
- this.update(this.changeLog, false);
- }
+ created() {
+ this.debouncedSave = debounce(() => this.save(), 500);
+ this.debouncedUpload = debounce(evt => this.upload(evt), 500);
},
methods: {
- removeChamp(item) {
- if (item.id) {
+ removeChamp() {
+ if (this.id) {
this.deleted = true;
+ this.debouncedSave();
} else {
- const index = this.state.typesDeChamp.indexOf(item);
+ const index = this.state.typesDeChamp.indexOf(this.item);
this.state.typesDeChamp.splice(index, 1);
- this.update([this.itemId, true]);
}
},
nameFor(name) {
@@ -132,6 +163,76 @@ export default {
type_champ: 'text',
types_de_champ: []
});
+ },
+ update() {
+ this.hasChanges = true;
+ if (this.isValid) {
+ if (this.state.inFlight === 0) {
+ this.state.flash.clear();
+ }
+ this.debouncedSave();
+ }
+ },
+ upload(evt) {
+ if (this.isUploading) {
+ this.debouncedUpload();
+ } else {
+ const input = evt.target;
+ const file = input.files[0];
+ if (file) {
+ this.isUploading = true;
+ uploadFile(this.state.directUploadUrl, file).then(({ signed_id }) => {
+ this.pieceJustificativeTemplate = signed_id;
+ this.isUploading = false;
+ this.debouncedSave();
+ });
+ }
+ input.value = null;
+ }
+ },
+ save() {
+ if (this.isSaving) {
+ this.debouncedSave();
+ } else {
+ this.isSaving = true;
+ this.state.inFlight++;
+ getJSON(this.saveUrl, this.savePayload, this.saveMethod)
+ .then(data => {
+ this.onSuccess(data);
+ })
+ .catch(xhr => {
+ this.onError(xhr);
+ });
+ }
+ },
+ onSuccess(data) {
+ if (data && data.type_de_champ) {
+ this.id = data.type_de_champ.id;
+ this.pieceJustificativeTemplateUrl =
+ data.type_de_champ.piece_justificative_template_url;
+ this.pieceJustificativeTemplateFilename =
+ data.type_de_champ.piece_justificative_template_filename;
+ this.pieceJustificativeTemplate = null;
+ }
+ this.state.inFlight--;
+ this.isSaving = false;
+ this.hasChanges = false;
+
+ if (this.state.inFlight === 0) {
+ this.state.flash.success();
+ }
+ },
+ onError(xhr) {
+ this.isSaving = false;
+ this.state.inFlight--;
+ try {
+ const {
+ errors: [message]
+ } = JSON.parse(xhr.responseText);
+ this.state.flash.error(message);
+ } catch (e) {
+ this.state.flash.error(xhr.responseText);
+ }
}
}
};
@@ -143,21 +244,20 @@ const EXCLUDE_FROM_REPETITION = [
'siret'
];
-const PATHS_TO_WATCH = [
- 'typeChamp',
- 'libelle',
- 'mandatory',
- 'description',
- 'dropDownList',
- 'options.quartiers_prioritaires',
- 'options.cadastres',
- 'options.parcelles_agricoles',
- 'index',
- 'deleted'
-];
-
function castBoolean(value) {
return value && value != 0;
}
-let clientIds = 0;
+function uploadFile(directUploadUrl, file) {
+ const upload = new DirectUpload(file, directUploadUrl);
+
+ return new Promise((resolve, reject) => {
+ upload.create((error, blob) => {
+ if (error) {
+ reject(error);
+ } else {
+ resolve(blob);
+ }
+ });
+ });
+}
diff --git a/app/javascript/new_design/administrateur/DraggableItem.vue b/app/javascript/new_design/administrateur/DraggableItem.vue
index fbb70a5c8..1351dc395 100644
--- a/app/javascript/new_design/administrateur/DraggableItem.vue
+++ b/app/javascript/new_design/administrateur/DraggableItem.vue
@@ -1,6 +1,6 @@
-
+
@@ -14,6 +14,7 @@
:id="elementIdFor('type_champ')"
:name="nameFor('type_champ')"
v-model="typeChamp"
+ @change="update"
class="small-margin small inline">
- Enregistrer
+ Enregistrer
@@ -33,7 +32,7 @@
- Enregistrer
+ Enregistrer
diff --git a/app/javascript/new_design/administrateur/champs-editor.js b/app/javascript/new_design/administrateur/champs-editor.js
index 3b47de908..62269faaf 100644
--- a/app/javascript/new_design/administrateur/champs-editor.js
+++ b/app/javascript/new_design/administrateur/champs-editor.js
@@ -1,6 +1,5 @@
import Vue from 'vue';
import Draggable from 'vuedraggable';
-import { fire, debounce } from '@utils';
import DraggableItem from './DraggableItem';
import DraggableList from './DraggableList';
@@ -16,109 +15,74 @@ addEventListener('DOMContentLoaded', () => {
});
function initEditor(el) {
- const { directUploadsUrl, dragIconUrl } = el.dataset;
+ const { directUploadUrl, dragIconUrl, saveUrl } = el.dataset;
const state = {
typesDeChamp: JSON.parse(el.dataset.typesDeChamp),
typesDeChampOptions: JSON.parse(el.dataset.typesDeChampOptions),
- directUploadsUrl,
+ directUploadUrl,
dragIconUrl,
+ saveUrl,
isAnnotation: el.dataset.type === 'annotation',
- unsavedItems: new Set(),
- unsavedInvalidItems: new Set(),
- version: 1,
- prefix: 'procedure'
+ prefix: 'procedure',
+ inFlight: 0,
+ flash: new Flash()
};
+ // We add an initial type de champ here if form is empty
+ if (state.typesDeChamp.length === 0) {
+ state.typesDeChamp.push({
+ type_champ: 'text',
+ types_de_champ: []
+ });
+ }
+
new Vue({
el,
data: {
- state,
- update: null
+ state
},
render(h) {
return h(DraggableList, {
props: {
- state: this.state,
- update: this.update,
- updateAll: this.updateAll
+ state: this.state
}
});
- },
- mounted() {
- const [update, updateAll] = createUpdateFunctions(
- this,
- state.isAnnotation
- );
-
- this.update = update;
- this.updateAll = updateAll;
-
- // We add an initial type de champ here if form is empty
- if (this.state.typesDeChamp.length === 0) {
- this.state.typesDeChamp.push({
- type_champ: 'text',
- types_de_champ: []
- });
- }
}
});
}
-function createUpdateFunctions(app, isAnnotation) {
- let isSaving = false;
- const form = app.$el.closest('form');
-
- const update = ([id, isValid], refresh = true) => {
- app.state.unsavedItems.add(id);
- if (isValid) {
- app.state.unsavedInvalidItems.delete(id);
+class Flash {
+ constructor(isAnnotation) {
+ this.element = document.querySelector('#flash_messages');
+ this.isAnnotation = isAnnotation;
+ }
+ success() {
+ if (this.isAnnotation) {
+ this.add('Annotations privées enregistrées.');
} else {
- app.state.unsavedInvalidItems.add(id);
+ this.add('Formulaire enregistré.');
}
- if (refresh) {
- app.state.version += 1;
- }
- updateAll();
- };
+ }
+ error(message) {
+ this.add(message, true);
+ }
+ clear() {
+ this.element.innerHTML = '';
+ }
+ add(message, isError) {
+ const html = `
+
+ ${message}
+
+
`;
- const updateAll = debounce(() => {
- if (isSaving) {
- updateAll();
- } else if (
- app.state.typesDeChamp.length > 0 &&
- app.state.unsavedInvalidItems.size === 0
- ) {
- isSaving = true;
- app.state.unsavedItems.clear();
- app.state.version += 1;
- fire(form, 'submit');
- }
- }, 500);
+ this.element.innerHTML = html;
- addEventListener('ProcedureUpdated', event => {
- const { types_de_champ, types_de_champ_private } = event.detail;
-
- app.state.typesDeChamp = isAnnotation
- ? types_de_champ_private
- : types_de_champ;
- isSaving = false;
- updateFileInputs();
- });
-
- return [update, updateAll];
-}
-
-// This is needed du to the way ActiveStorage javascript integration works.
-// It is built to be used with traditional forms. Another way would be to not use
-// high level ActiveStorage abstractions (and maybe this is what we should do in the future).
-function updateFileInputs() {
- for (let element of document.querySelectorAll('.direct-upload')) {
- let hiddenInput = element.nextElementSibling;
- let fileInput = hiddenInput.nextElementSibling;
- element.remove();
- hiddenInput.remove();
- fileInput.value = '';
- fileInput.removeAttribute('disabled');
+ setTimeout(() => {
+ this.clear();
+ }, 6000);
}
}
diff --git a/app/javascript/shared/autocomplete.js b/app/javascript/shared/autocomplete.js
index f5cf5861c..db9db0180 100644
--- a/app/javascript/shared/autocomplete.js
+++ b/app/javascript/shared/autocomplete.js
@@ -38,13 +38,26 @@ function source(url) {
}
addEventListener('turbolinks:load', function() {
+ autocompleteSetup();
+});
+
+addEventListener('ajax:success', function() {
+ autocompleteSetup();
+});
+
+function autocompleteSetup() {
for (let { type, url } of sources) {
- for (let target of document.querySelectorAll(selector(type))) {
- let select = autocomplete(target, options, [source(url)]);
- select.on('autocomplete:selected', ({ target }, suggestion) => {
- fire(target, 'autocomplete:select', suggestion);
- select.autocomplete.setVal(suggestion.label);
- });
+ for (let element of document.querySelectorAll(selector(type))) {
+ element.removeAttribute('data-autocomplete');
+ autocompleteInitializeElement(element, url);
}
}
-});
+}
+
+function autocompleteInitializeElement(element, url) {
+ const select = autocomplete(element, options, [source(url)]);
+ select.on('autocomplete:selected', ({ target }, suggestion) => {
+ fire(target, 'autocomplete:select', suggestion);
+ select.autocomplete.setVal(suggestion.label);
+ });
+}
diff --git a/app/lib/api_carto/api.rb b/app/lib/api_carto/api.rb
index 84bcd5d70..f94c11e2d 100644
--- a/app/lib/api_carto/api.rb
+++ b/app/lib/api_carto/api.rb
@@ -15,7 +15,7 @@ class ApiCarto::API
params = geojson.to_s
RestClient.post(url, params, content_type: 'application/json')
- rescue RestClient::InternalServerError, RestClient::BadGateway, RestClient::GatewayTimeout => e
+ rescue RestClient::InternalServerError, RestClient::BadGateway, RestClient::GatewayTimeout, RestClient::ServiceUnavailable => e
Rails.logger.error "[ApiCarto] Error on #{url}: #{e}"
raise RestClient::ResourceNotFound
end
diff --git a/app/models/dossier.rb b/app/models/dossier.rb
index f35370ed4..70f023e26 100644
--- a/app/models/dossier.rb
+++ b/app/models/dossier.rb
@@ -120,17 +120,11 @@ class Dossier < ApplicationRecord
end
def build_default_champs
- procedure.types_de_champ.each do |type_de_champ|
- champ = type_de_champ.champ.build
-
- if type_de_champ.repetition?
- champ.add_row
- end
-
+ procedure.build_champs.each do |champ|
champs << champ
end
- procedure.types_de_champ_private.each do |type_de_champ|
- champs_private << type_de_champ.champ.build
+ procedure.build_champs_private.each do |champ|
+ champs_private << champ
end
end
@@ -166,7 +160,11 @@ class Dossier < ApplicationRecord
!procedure.archivee? && brouillon?
end
- def can_be_updated_by_the_user?
+ def can_be_updated_by_user?
+ brouillon? || en_construction?
+ end
+
+ def can_be_deleted_by_user?
brouillon? || en_construction?
end
diff --git a/app/models/procedure.rb b/app/models/procedure.rb
index 7fd28e5ed..c48b3ff37 100644
--- a/app/models/procedure.rb
+++ b/app/models/procedure.rb
@@ -138,10 +138,15 @@ class Procedure < ApplicationRecord
# Warning: dossier after_save build_default_champs must be removed
# to save a dossier created from this method
def new_dossier
- champs = types_de_champ.map { |tdc| tdc.champ.build }
- champs_private = types_de_champ_private.map { |tdc| tdc.champ.build }
+ Dossier.new(procedure: self, champs: build_champs, champs_private: build_champs_private)
+ end
- Dossier.new(procedure: self, champs: champs, champs_private: champs_private)
+ def build_champs
+ types_de_champ.map(&:build_champ)
+ end
+
+ def build_champs_private
+ types_de_champ_private.map(&:build_champ)
end
def default_path
diff --git a/app/models/type_de_champ.rb b/app/models/type_de_champ.rb
index 727767c6e..e88043a6e 100644
--- a/app/models/type_de_champ.rb
+++ b/app/models/type_de_champ.rb
@@ -120,6 +120,10 @@ class TypeDeChamp < ApplicationRecord
}
end
+ def build_champ
+ dynamic_type.build_champ
+ end
+
def self.type_de_champs_list_fr
type_champs.map { |champ| [I18n.t("activerecord.attributes.type_de_champ.type_champs.#{champ.last}"), champ.first] }
end
@@ -175,6 +179,14 @@ class TypeDeChamp < ApplicationRecord
end
end
+ def drop_down_list_value
+ drop_down_list&.value
+ end
+
+ def drop_down_list_value=(value)
+ self.drop_down_list_attributes = { value: value }
+ end
+
private
def setup_procedure
diff --git a/app/models/types_de_champ/repetition_type_de_champ.rb b/app/models/types_de_champ/repetition_type_de_champ.rb
index 5abf8efd3..c3328850a 100644
--- a/app/models/types_de_champ/repetition_type_de_champ.rb
+++ b/app/models/types_de_champ/repetition_type_de_champ.rb
@@ -1,2 +1,7 @@
class TypesDeChamp::RepetitionTypeDeChamp < TypesDeChamp::TypeDeChampBase
+ def build_champ
+ champ = super
+ champ.add_row
+ champ
+ end
end
diff --git a/app/models/types_de_champ/type_de_champ_base.rb b/app/models/types_de_champ/type_de_champ_base.rb
index fb05bd2ab..410663aa3 100644
--- a/app/models/types_de_champ/type_de_champ_base.rb
+++ b/app/models/types_de_champ/type_de_champ_base.rb
@@ -19,4 +19,8 @@ class TypesDeChamp::TypeDeChampBase
}
]
end
+
+ def build_champ
+ @type_de_champ.champ.build
+ end
end
diff --git a/app/views/new_administrateur/procedures/update.js.erb b/app/views/new_administrateur/procedures/update.js.erb
deleted file mode 100644
index d8423e18a..000000000
--- a/app/views/new_administrateur/procedures/update.js.erb
+++ /dev/null
@@ -1,3 +0,0 @@
-<%= render_flash timeout: 6000, fixed: true %>
-
-<%= fire_event(:ProcedureUpdated, procedure_data(@procedure.reload)) %>
diff --git a/app/views/new_user/dossiers/index.html.haml b/app/views/new_user/dossiers/index.html.haml
index 0c83b7eeb..b9469c72c 100644
--- a/app/views/new_user/dossiers/index.html.haml
+++ b/app/views/new_user/dossiers/index.html.haml
@@ -51,7 +51,7 @@
= link_to(url_for_dossier(dossier), class: 'cell-link') do
= dossier.updated_at.strftime("%d/%m/%Y")
%td.action-col.delete-col
- - if dossier.brouillon?
+ - if dossier.can_be_deleted_by_user?
= link_to(ask_deletion_dossier_path(dossier), method: :post, class: 'button danger', data: { disable: true, confirm: "En continuant, vous allez supprimer ce dossier ainsi que les informations qu’il contient. Toute suppression entraine l’annulation de la démarche en cours.\n\nConfirmer la suppression ?" }) do
%span.icon.delete
Supprimer
diff --git a/app/views/shared/dossiers/editable_champs/_repetition.html.haml b/app/views/shared/dossiers/editable_champs/_repetition.html.haml
index d7e8717c7..7f25613da 100644
--- a/app/views/shared/dossiers/editable_champs/_repetition.html.haml
+++ b/app/views/shared/dossiers/editable_champs/_repetition.html.haml
@@ -5,11 +5,16 @@
= form.fields_for :champs, champ do |form|
= render partial: 'shared/dossiers/editable_champs/editable_champ', locals: { champ: form.object, form: form }
= form.hidden_field :_destroy, disabled: true
- %button.button.danger.remove-row
- Supprimer
+ .flex.row-reverse
+ - if champ.persisted?
+ %button.button.danger.remove-row
+ Supprimer
+ - else
+ %button.button.danger{ type: :button }
+ Supprimer
- if champ.persisted?
= link_to "Ajouter une ligne pour « #{champ.libelle} »", champs_repetition_path(form.index), class: 'button add-row', data: { remote: true, method: 'POST', params: { champ_id: champ&.id }.to_query }
- else
- %button.button.add-row{ disabled: true }
+ %button.button{ type: :button }
= "Ajouter une ligne pour « #{champ.libelle} »"
diff --git a/config/routes.rb b/config/routes.rb
index c0b41153f..6e0f65c98 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -367,6 +367,8 @@ Rails.application.routes.draw do
get 'champs'
get 'annotations'
end
+
+ resources :types_de_champ, only: [:create, :update, :destroy]
end
resources :services, except: [:show] do
diff --git a/spec/controllers/new_user/dossiers_controller_spec.rb b/spec/controllers/new_user/dossiers_controller_spec.rb
index bc08ba78b..198934abb 100644
--- a/spec/controllers/new_user/dossiers_controller_spec.rb
+++ b/spec/controllers/new_user/dossiers_controller_spec.rb
@@ -798,13 +798,13 @@ describe NewUser::DossiersController, type: :controller do
subject { post :ask_deletion, params: { id: dossier.id } }
shared_examples_for "the dossier can not be deleted" do
- it do
+ it "doesn’t notify the deletion" do
expect(DossierMailer).not_to receive(:notify_deletion_to_administration)
expect(DossierMailer).not_to receive(:notify_deletion_to_user)
subject
end
- it do
+ it "doesn’t delete the dossier" do
subject
expect(Dossier.find_by(id: dossier.id)).not_to eq(nil)
expect(dossier.procedure.deleted_dossiers.count).to eq(0)
@@ -814,13 +814,13 @@ describe NewUser::DossiersController, type: :controller do
context 'when dossier is owned by signed in user' do
let(:dossier) { create(:dossier, :en_construction, user: user, autorisation_donnees: true) }
- it do
+ it "notifies the user and the admin of the deletion" do
expect(DossierMailer).to receive(:notify_deletion_to_administration).with(kind_of(DeletedDossier), dossier.procedure.administrateur.email).and_return(double(deliver_later: nil))
expect(DossierMailer).to receive(:notify_deletion_to_user).with(kind_of(DeletedDossier), dossier.user.email).and_return(double(deliver_later: nil))
subject
end
- it do
+ it "deletes the dossier" do
procedure = dossier.procedure
dossier_id = dossier.id
subject
diff --git a/spec/features/admin/procedure_creation_spec.rb b/spec/features/admin/procedure_creation_spec.rb
index eefc0853f..0e0dbd5cc 100644
--- a/spec/features/admin/procedure_creation_spec.rb
+++ b/spec/features/admin/procedure_creation_spec.rb
@@ -102,7 +102,8 @@ feature 'As an administrateur I wanna create a new procedure', js: true do
expect(page).to have_selector('#procedure_types_de_champ_attributes_0_libelle')
fill_in 'procedure_types_de_champ_attributes_0_libelle', with: 'libelle de champ'
- expect(page).to have_content('Formulaire mis à jour')
+ blur
+ expect(page).to have_content('Formulaire enregistré')
within '.footer' do
click_on 'Ajouter un champ'
@@ -129,7 +130,8 @@ feature 'As an administrateur I wanna create a new procedure', js: true do
page.refresh
fill_in 'procedure_types_de_champ_attributes_0_libelle', with: 'libelle de champ'
- expect(page).to have_content('Formulaire mis à jour')
+ blur
+ expect(page).to have_content('Formulaire enregistré')
click_on Procedure.last.libelle
click_on 'onglet-pieces'
diff --git a/spec/features/new_administrateur/procedures_spec.rb b/spec/features/new_administrateur/types_de_champ_spec.rb
similarity index 67%
rename from spec/features/new_administrateur/procedures_spec.rb
rename to spec/features/new_administrateur/types_de_champ_spec.rb
index c1b3b1b8b..288d4f950 100644
--- a/spec/features/new_administrateur/procedures_spec.rb
+++ b/spec/features/new_administrateur/types_de_champ_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'As an administrateur I edit procedure', js: true do
+feature 'As an administrateur I can edit types de champ', js: true do
let(:administrateur) { procedure.administrateur }
let(:procedure) { create(:procedure) }
@@ -18,14 +18,15 @@ feature 'As an administrateur I edit procedure', js: true do
end
expect(page).to have_selector('#procedure_types_de_champ_attributes_0_libelle')
fill_in 'procedure_types_de_champ_attributes_0_libelle', with: 'libellé de champ'
- expect(page).to have_content('Formulaire mis à jour')
+ blur
+ expect(page).to have_content('Formulaire enregistré')
page.refresh
within '.footer' do
click_on 'Enregistrer'
end
- expect(page).to have_content('Formulaire mis à jour')
+ expect(page).to have_content('Formulaire enregistré')
end
it "Add multiple champs" do
@@ -34,33 +35,29 @@ feature 'As an administrateur I edit procedure', js: true do
click_on 'Ajouter un champ'
click_on 'Ajouter un champ'
end
- expect(page).not_to have_content('Le libellé doit être rempli.')
- expect(page).not_to have_content('Modifications non sauvegardées.')
- expect(page).not_to have_content('Formulaire mis à jour')
+ expect(page).not_to have_content('Formulaire enregistré')
+
fill_in 'procedure_types_de_champ_attributes_0_libelle', with: 'libellé de champ 0'
+ fill_in 'procedure_types_de_champ_attributes_1_libelle', with: 'libellé de champ 1'
+ blur
+ expect(page).to have_content('Formulaire enregistré')
expect(page).to have_selector('#procedure_types_de_champ_attributes_0_libelle')
expect(page).to have_selector('#procedure_types_de_champ_attributes_1_libelle')
expect(page).to have_selector('#procedure_types_de_champ_attributes_2_libelle')
expect(page).to have_selector('#procedure_types_de_champ_attributes_3_libelle')
- expect(page).to have_content('Le libellé doit être rempli.')
- expect(page).to have_content('Modifications non sauvegardées.')
- expect(page).not_to have_content('Formulaire mis à jour')
- fill_in 'procedure_types_de_champ_attributes_2_libelle', with: 'libellé de champ 2'
-
- within '.draggable-item-3' do
+ within '.draggable-item-2' do
click_on 'Supprimer'
end
- expect(page).to have_content('Le libellé doit être rempli.')
- expect(page).to have_content('Modifications non sauvegardées.')
- expect(page).not_to have_content('Formulaire mis à jour')
- fill_in 'procedure_types_de_champ_attributes_1_libelle', with: 'libellé de champ 1'
+ expect(page).not_to have_selector('#procedure_types_de_champ_attributes_3_libelle')
+ fill_in 'procedure_types_de_champ_attributes_2_libelle', with: 'libellé de champ 2'
+ blur
+ expect(page).to have_content('Formulaire enregistré')
+
+ expect(page).to have_content('Supprimer', count: 3)
- expect(page).not_to have_content('Le libellé doit être rempli.')
- expect(page).not_to have_content('Modifications non sauvegardées.')
- expect(page).to have_content('Formulaire mis à jour')
page.refresh
expect(page).to have_content('Supprimer', count: 3)
@@ -68,11 +65,12 @@ feature 'As an administrateur I edit procedure', js: true do
it "Remove champs" do
fill_in 'procedure_types_de_champ_attributes_0_libelle', with: 'libellé de champ'
- expect(page).to have_content('Formulaire mis à jour')
+ blur
+ expect(page).to have_content('Formulaire enregistré')
page.refresh
click_on 'Supprimer'
- expect(page).to have_content('Formulaire mis à jour')
+ expect(page).to have_content('Formulaire enregistré')
expect(page).not_to have_content('Supprimer')
page.refresh
@@ -82,19 +80,21 @@ feature 'As an administrateur I edit procedure', js: true do
it "Only add valid champs" do
expect(page).to have_selector('#procedure_types_de_champ_attributes_0_description')
fill_in 'procedure_types_de_champ_attributes_0_description', with: 'déscription du champ'
- expect(page).to have_content('Le libellé doit être rempli.')
- expect(page).not_to have_content('Formulaire mis à jour')
+ blur
+ expect(page).not_to have_content('Formulaire enregistré')
fill_in 'procedure_types_de_champ_attributes_0_libelle', with: 'libellé de champ'
- expect(page).to have_content('Formulaire mis à jour')
+ blur
+ expect(page).to have_content('Formulaire enregistré')
end
it "Add repetition champ" do
expect(page).to have_selector('#procedure_types_de_champ_attributes_0_libelle')
select('Bloc répétable', from: 'procedure_types_de_champ_attributes_0_type_champ')
fill_in 'procedure_types_de_champ_attributes_0_libelle', with: 'libellé de champ'
+ blur
- expect(page).to have_content('Formulaire mis à jour')
+ expect(page).to have_content('Formulaire enregistré')
page.refresh
within '.flex-grow' do
@@ -102,8 +102,9 @@ feature 'As an administrateur I edit procedure', js: true do
end
fill_in 'procedure_types_de_champ_attributes_0_types_de_champ_attributes_0_libelle', with: 'libellé de champ 1'
+ blur
- expect(page).to have_content('Formulaire mis à jour')
+ expect(page).to have_content('Formulaire enregistré')
expect(page).to have_content('Supprimer', count: 2)
within '.footer' do
@@ -112,6 +113,7 @@ feature 'As an administrateur I edit procedure', js: true do
select('Bloc répétable', from: 'procedure_types_de_champ_attributes_1_type_champ')
fill_in 'procedure_types_de_champ_attributes_1_libelle', with: 'libellé de champ 2'
+ blur
expect(page).to have_content('Supprimer', count: 3)
end
diff --git a/spec/features/new_user/list_dossiers_spec.rb b/spec/features/new_user/list_dossiers_spec.rb
index f47133bac..ac0aeb80c 100644
--- a/spec/features/new_user/list_dossiers_spec.rb
+++ b/spec/features/new_user/list_dossiers_spec.rb
@@ -1,13 +1,13 @@
require 'spec_helper'
-describe 'user access to the list of his dossier' do
+describe 'user access to the list of their dossiers' do
let(:user) { create(:user) }
- let!(:last_updated_dossier) { create(:dossier, :with_entreprise, user: user, state: Dossier.states.fetch(:en_construction)) }
- let!(:dossier1) { create(:dossier, :with_entreprise, user: user, state: Dossier.states.fetch(:en_construction)) }
- let!(:dossier2) { create(:dossier, :with_entreprise) }
- let!(:dossier_brouillon) { create(:dossier, :with_entreprise, user: user) }
- let!(:dossier_archived) { create(:dossier, :with_entreprise, user: user, state: Dossier.states.fetch(:en_construction)) }
+ let!(:dossier_brouillon) { create(:dossier, user: user) }
+ let!(:dossier_en_construction) { create(:dossier, :en_construction, user: user) }
+ let!(:dossier_en_instruction) { create(:dossier, :en_instruction, user: user) }
+ let!(:dossier_archived) { create(:dossier, :en_instruction, :archived, user: user) }
let(:dossiers_per_page) { 25 }
+ let(:last_updated_dossier) { dossier_en_construction }
before do
@default_per_page = Dossier.default_per_page
@@ -15,12 +15,8 @@ describe 'user access to the list of his dossier' do
last_updated_dossier.update_column(:updated_at, "19/07/2052 15:35".to_time)
- visit new_user_session_path
- within('#new_user') do
- page.find_by_id('user_email').set user.email
- page.find_by_id('user_password').set user.password
- page.click_on 'Se connecter'
- end
+ login_as user, scope: :user
+ visit dossiers_path
end
after do
@@ -28,52 +24,59 @@ describe 'user access to the list of his dossier' do
end
it 'the list of dossier is displayed' do
- expect(page).to have_content(dossier1.procedure.libelle)
- expect(page).to have_content('en construction')
- end
-
- it 'dossiers belonging to other users are not displayed' do
- expect(page).not_to have_content(dossier2.procedure.libelle)
- end
-
- it 'the list must be ordered by last updated' do
- expect(page.body).to match(/#{last_updated_dossier.procedure.libelle}.*#{dossier1.procedure.libelle}/m)
- end
-
- it 'should list archived dossiers' do
+ expect(page).to have_content(dossier_brouillon.procedure.libelle)
+ expect(page).to have_content(dossier_en_construction.procedure.libelle)
+ expect(page).to have_content(dossier_en_instruction.procedure.libelle)
expect(page).to have_content(dossier_archived.procedure.libelle)
end
- it 'should have link to only delete brouillon' do
- expect(page).to have_link(nil, href: ask_deletion_dossier_path(dossier_brouillon))
- expect(page).not_to have_link(nil, href: ask_deletion_dossier_path(dossier1))
+ it 'the list must be ordered by last updated' do
+ expect(page.body).to match(/#{last_updated_dossier.procedure.libelle}.*#{dossier_en_instruction.procedure.libelle}/m)
end
- context 'when user clicks on delete brouillon', js: true do
- scenario 'dossier is deleted' do
- page.accept_alert('Confirmer la suppression ?') do
- find(:xpath, "//a[@href='#{ask_deletion_dossier_path(dossier_brouillon)}']").click
- end
+ context 'when there are dossiers from other users' do
+ let!(:dossier_other_user) { create(:dossier) }
- expect(page).to have_content('Votre dossier a bien été supprimé.')
- end
- end
-
- context 'when user clicks on a projet in list', js: true do
- before do
- page.click_on(dossier1.procedure.libelle)
- end
- scenario 'user is redirected to dossier page' do
- expect(page).to have_current_path(dossier_path(dossier1))
+ it 'doesn’t display dossiers belonging to other users' do
+ expect(page).not_to have_content(dossier_other_user.procedure.libelle)
end
end
context 'when there is more than one page' do
let(:dossiers_per_page) { 2 }
- scenario 'the user can navigate through the other pages', js: true do
+ scenario 'the user can navigate through the other pages' do
+ expect(page).not_to have_content(dossier_en_instruction.procedure.libelle)
page.click_link("Suivant")
- expect(page).to have_content(dossier_archived.procedure.libelle)
+ expect(page).to have_content(dossier_en_instruction.procedure.libelle)
+ end
+ end
+
+ context 'when user clicks on a projet in list' do
+ before do
+ page.click_on(dossier_en_construction.procedure.libelle)
+ end
+
+ scenario 'user is redirected to dossier page' do
+ expect(page).to have_current_path(dossier_path(dossier_en_construction))
+ end
+ end
+
+ describe 'deletion' do
+ it 'should have links to delete dossiers' do
+ expect(page).to have_link(nil, href: ask_deletion_dossier_path(dossier_brouillon))
+ expect(page).to have_link(nil, href: ask_deletion_dossier_path(dossier_en_construction))
+ expect(page).not_to have_link(nil, href: ask_deletion_dossier_path(dossier_en_instruction))
+ end
+
+ context 'when user clicks on delete button', js: true do
+ scenario 'the dossier is deleted' do
+ page.accept_alert('Confirmer la suppression ?') do
+ find(:xpath, "//a[@href='#{ask_deletion_dossier_path(dossier_brouillon)}']").click
+ end
+
+ expect(page).to have_content('Votre dossier a bien été supprimé.')
+ end
end
end
@@ -91,25 +94,27 @@ describe 'user access to the list of his dossier' do
end
context "when the dossier does not belong to the user" do
+ let!(:dossier_other_user) { create(:dossier) }
+
before do
- page.find_by_id('dossier_id').set(dossier2.id)
+ page.find_by_id('dossier_id').set(dossier_other_user.id)
click_button("Rechercher")
end
it "shows an error message on the dossiers page" do
expect(current_path).to eq(dossiers_path)
- expect(page).to have_content("Vous n’avez pas de dossier avec le nº #{dossier2.id}.")
+ expect(page).to have_content("Vous n’avez pas de dossier avec le nº #{dossier_other_user.id}.")
end
end
context "when the dossier belongs to the user" do
before do
- page.find_by_id('dossier_id').set(dossier1.id)
+ page.find_by_id('dossier_id').set(dossier_en_construction.id)
click_button("Rechercher")
end
it "redirects to the dossier page" do
- expect(current_path).to eq(dossier_path(dossier1))
+ expect(current_path).to eq(dossier_path(dossier_en_construction))
end
end
end
diff --git a/spec/support/feature_helpers.rb b/spec/support/feature_helpers.rb
index b1170def9..ee4dcd103 100644
--- a/spec/support/feature_helpers.rb
+++ b/spec/support/feature_helpers.rb
@@ -58,6 +58,10 @@ module FeatureHelpers
# Procedure contact infos in the footer
expect(page).to have_content(procedure.service.email)
end
+
+ def blur
+ page.find('body').click
+ end
end
RSpec.configure do |config|