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 @@