Merge pull request #3408 from betagouv/dev

2019-02-11-01
This commit is contained in:
Pierre de La Morinerie 2019-02-11 11:48:25 +01:00 committed by GitHub
commit 0b58bed658
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
27 changed files with 480 additions and 336 deletions

View file

@ -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 : 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 linfrastructure 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 linfrastructure 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.
Cest également valable pour le stockage des pièces-jointes, qui sont souvent des documents didentités dont la confidentialité doit être garantie. Cest é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 sinterconnecte à 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, lanalyse anti-virus ou lenvoi des emails. Le fonctionnement de demarches-simplifiees.fr dépend de la disponibilité de ces services externes. - **Utilisation de services externes** : demarches-simplifiees.fr sinterconnecte à 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, lanalyse anti-virus ou lenvoi 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 lorsquune faille de sécurité est signalée. Ces mises à jour fréquentes en production sont indispensables au bon fonctionnement de loutil. - **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 lorsquune faille de sécurité est signalée. Ces mises à jour fréquentes en production sont indispensables au bon fonctionnement de loutil.
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 dhéberger une autre instance vous-même**. 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 dhéberger une autre instance vous-même**.
Dans le cas où vous envisagez dhé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 dhé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 dhé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.

View file

@ -32,6 +32,10 @@
&.column { &.column {
flex-direction: column; flex-direction: column;
} }
&.row-reverse {
flex-direction: row-reverse;
}
} }
.flex-grow { .flex-grow {

View file

@ -1,49 +1,13 @@
module NewAdministrateur module NewAdministrateur
class ProceduresController < AdministrateurController class ProceduresController < AdministrateurController
before_action :retrieve_procedure, only: [:champs, :annotations, :update] before_action :retrieve_procedure, only: [:champs, :annotations]
before_action :procedure_locked?, only: [:champs, :annotations, :update] before_action :procedure_locked?, only: [:champs, :annotations]
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
}
def apercu def apercu
@dossier = procedure_without_control.new_dossier @dossier = procedure_without_control.new_dossier
@tab = apercu_tab @tab = apercu_tab
end 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 private
def apercu_tab def apercu_tab
@ -53,12 +17,5 @@ module NewAdministrateur
def procedure_without_control def procedure_without_control
Procedure.find(params[:id]) Procedure.find(params[:id])
end 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
end end

View file

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

View file

@ -188,7 +188,7 @@ module NewUser
def ask_deletion def ask_deletion
dossier = current_user.dossiers.includes(:user, procedure: :administrateur).find(params[:id]) 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 dossier.delete_and_keep_track
flash.notice = 'Votre dossier a bien été supprimé.' flash.notice = 'Votre dossier a bien été supprimé.'
redirect_to dossiers_path redirect_to dossiers_path
@ -257,7 +257,7 @@ module NewUser
end end
def ensure_dossier_can_be_updated 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é' flash.alert = 'Votre dossier ne peut plus être modifié'
redirect_to dossiers_path redirect_to dossiers_path
end end

View file

@ -39,7 +39,8 @@ module ProcedureHelper
type: "champ", type: "champ",
types_de_champ_options: types_de_champ_options.to_json, types_de_champ_options: types_de_champ_options.to_json,
types_de_champ: types_de_champ_as_json(procedure.types_de_champ).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") drag_icon_url: image_url("icons/drag.svg")
} }
end end
@ -49,18 +50,12 @@ module ProcedureHelper
type: "annotation", type: "annotation",
types_de_champ_options: types_de_champ_options.to_json, 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, 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") drag_icon_url: image_url("icons/drag.svg")
} }
end 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 private
TOGGLES = { TOGGLES = {
@ -79,14 +74,12 @@ module ProcedureHelper
types_de_champ types_de_champ
end end
TYPES_DE_CHAMP_INCLUDE = { drop_down_list: { only: :value } }
TYPES_DE_CHAMP_BASE = { TYPES_DE_CHAMP_BASE = {
except: [:created_at, :updated_at, :stable_id, :type, :parent_id, :procedure_id, :private], except: [:created_at, :updated_at, :stable_id, :type, :parent_id, :procedure_id, :private],
methods: [:piece_justificative_template_filename, :piece_justificative_template_url], methods: [:piece_justificative_template_filename, :piece_justificative_template_url, :drop_down_list_value]
include: TYPES_DE_CHAMP_INCLUDE
} }
TYPES_DE_CHAMP = TYPES_DE_CHAMP_BASE 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) def types_de_champ_as_json(types_de_champ)
types_de_champ.includes(:drop_down_list, types_de_champ.includes(:drop_down_list,

View file

@ -1,40 +1,23 @@
import { getJSON, debounce } from '@utils';
import { DirectUpload } from 'activestorage';
export default { export default {
props: ['state', 'update', 'index', 'item'], props: ['state', 'index', 'item'],
computed: { computed: {
isDirty() { isValid() {
return (
this.state.version &&
this.state.unsavedInvalidItems.size > 0 &&
this.state.unsavedItems.has(this.itemId)
);
},
isInvalid() {
if (this.deleted) { if (this.deleted) {
return false; return true;
} }
if (this.libelle) { if (this.libelle) {
return !this.libelle.trim(); return !!this.libelle.trim();
} }
return true; return false;
},
itemId() {
return this.item.id || this.clientId;
},
changeLog() {
return [this.itemId, !this.isInvalid];
}, },
itemClassName() { itemClassName() {
const classNames = [`draggable-item-${this.index}`]; const classNames = [`draggable-item-${this.index}`];
if (this.isHeaderSection) { if (this.isHeaderSection) {
classNames.push('type-header-section'); classNames.push('type-header-section');
} }
if (this.isDirty) {
if (this.isInvalid) {
classNames.push('invalid');
} else {
classNames.push('dirty');
}
}
return classNames.join(' '); return classNames.join(' ');
}, },
isDropDown() { isDropDown() {
@ -73,6 +56,47 @@ export default {
return 'types_de_champ_attributes'; 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() { typesDeChamp() {
return this.item.types_de_champ; return this.item.types_de_champ;
}, },
@ -85,39 +109,46 @@ export default {
return Object.assign({}, this.state, { return Object.assign({}, this.state, {
typesDeChamp: this.typesDeChamp, typesDeChamp: this.typesDeChamp,
typesDeChampOptions: this.typesDeChampOptions, typesDeChampOptions: this.typesDeChampOptions,
prefix: `${this.state.prefix}[${this.attribute}][${this.index}]` prefix: `${this.state.prefix}[${this.attribute}][${this.index}]`,
parentId: this.id
}); });
} }
}, },
data() { data() {
return { return {
id: this.item.id,
typeChamp: this.item.type_champ, typeChamp: this.item.type_champ,
libelle: this.item.libelle, libelle: this.item.libelle,
mandatory: this.item.mandatory, mandatory: this.item.mandatory,
description: this.item.description, 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, deleted: false,
clientId: `id-${clientIds++}` isSaving: false,
isUploading: false,
hasChanges: false
}; };
}, },
created() { watch: {
for (let path of PATHS_TO_WATCH) { index() {
this.$watch(path, () => this.update(this.changeLog)); this.update();
} }
}, },
mounted() { created() {
if (this.isInvalid) { this.debouncedSave = debounce(() => this.save(), 500);
this.update(this.changeLog, false); this.debouncedUpload = debounce(evt => this.upload(evt), 500);
}
}, },
methods: { methods: {
removeChamp(item) { removeChamp() {
if (item.id) { if (this.id) {
this.deleted = true; this.deleted = true;
this.debouncedSave();
} else { } else {
const index = this.state.typesDeChamp.indexOf(item); const index = this.state.typesDeChamp.indexOf(this.item);
this.state.typesDeChamp.splice(index, 1); this.state.typesDeChamp.splice(index, 1);
this.update([this.itemId, true]);
} }
}, },
nameFor(name) { nameFor(name) {
@ -132,6 +163,76 @@ export default {
type_champ: 'text', type_champ: 'text',
types_de_champ: [] 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' 'siret'
]; ];
const PATHS_TO_WATCH = [
'typeChamp',
'libelle',
'mandatory',
'description',
'dropDownList',
'options.quartiers_prioritaires',
'options.cadastres',
'options.parcelles_agricoles',
'index',
'deleted'
];
function castBoolean(value) { function castBoolean(value) {
return value && value != 0; 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);
}
});
});
}

View file

@ -1,6 +1,6 @@
<template> <template>
<div class="deleted" v-if="deleted"> <div class="deleted" v-if="deleted">
<input type="hidden" :name="nameFor('id')" :value="item.id"> <input type="hidden" :name="nameFor('id')" :value="id">
<input type="hidden" :name="nameFor('_destroy')" value="true"> <input type="hidden" :name="nameFor('_destroy')" value="true">
</div> </div>
@ -14,6 +14,7 @@
:id="elementIdFor('type_champ')" :id="elementIdFor('type_champ')"
:name="nameFor('type_champ')" :name="nameFor('type_champ')"
v-model="typeChamp" v-model="typeChamp"
@change="update"
class="small-margin small inline"> class="small-margin small inline">
<option v-for="option in state.typesDeChampOptions" :key="option[1]" :value="option[1]"> <option v-for="option in state.typesDeChampOptions" :key="option[1]" :value="option[1]">
{{ option[0] }} {{ option[0] }}
@ -21,21 +22,7 @@
</select> </select>
</div> </div>
<div class="flex justify-start delete"> <div class="flex justify-start delete">
<div v-if="isDirty" class="error-message"> <button class="button danger" @click.prevent="removeChamp">
<span v-if="isInvalid" class="content">
Le libellé doit être rempli.
</span>
<span v-else class="content">
<template v-if="state.isAnnotation">
Modifications non sauvegardées. Le libellé doit être rempli sur tous les annotations.
</template>
<template v-else>
Modifications non sauvegardées. Le libellé doit être rempli sur tous les champs.
</template>
</span>
</div>
<button class="button danger" @click.prevent="removeChamp(item)">
Supprimer Supprimer
</button> </button>
</div> </div>
@ -51,8 +38,9 @@
:id="elementIdFor('libelle')" :id="elementIdFor('libelle')"
:name="nameFor('libelle')" :name="nameFor('libelle')"
v-model="libelle" v-model="libelle"
@change="update"
class="small-margin small" class="small-margin small"
:class="{ error: isDirty && isInvalid }"> :class="{ error: hasChanges && !isValid }">
</div> </div>
<div class="cell" v-show="!isHeaderSection && !isExplication && !state.isAnnotation"> <div class="cell" v-show="!isHeaderSection && !isExplication && !state.isAnnotation">
@ -65,6 +53,7 @@
:id="elementIdFor('mandatory')" :id="elementIdFor('mandatory')"
:name="nameFor('mandatory')" :name="nameFor('mandatory')"
v-model="mandatory" v-model="mandatory"
@change="update"
class="small-margin small" class="small-margin small"
value="1"> value="1">
</div> </div>
@ -78,6 +67,7 @@
:id="elementIdFor('description')" :id="elementIdFor('description')"
:name="nameFor('description')" :name="nameFor('description')"
v-model="description" v-model="description"
@change="update"
rows=3 rows=3
cols=40 cols=40
class="small-margin small"> class="small-margin small">
@ -93,7 +83,8 @@
<textarea <textarea
:id="elementIdFor('drop_down_list')" :id="elementIdFor('drop_down_list')"
:name="nameFor('drop_down_list_attributes[value]')" :name="nameFor('drop_down_list_attributes[value]')"
v-model="dropDownList" v-model="dropDownListValue"
@change="update"
rows=3 rows=3
cols=40 cols=40
placeholder="Ecrire une valeur par ligne et --valeur-- pour un séparateur." placeholder="Ecrire une valeur par ligne et --valeur-- pour un séparateur."
@ -104,9 +95,9 @@
<label :for="elementIdFor('piece_justificative_template')"> <label :for="elementIdFor('piece_justificative_template')">
Modèle Modèle
</label> </label>
<template v-if="item.piece_justificative_template_url"> <template v-if="pieceJustificativeTemplateUrl">
<a :href="item.piece_justificative_template_url" target="_blank"> <a :href="pieceJustificativeTemplateUrl" target="_blank">
{{item.piece_justificative_template_filename}} {{pieceJustificativeTemplateFilename}}
</a> </a>
<br> Modifier : <br> Modifier :
</template> </template>
@ -114,8 +105,7 @@
type="file" type="file"
:id="elementIdFor('piece_justificative_template')" :id="elementIdFor('piece_justificative_template')"
:name="nameFor('piece_justificative_template')" :name="nameFor('piece_justificative_template')"
:data-direct-upload-url="state.directUploadsUrl" @change="upload"
@change="update(changeLog)"
class="small-margin small"> class="small-margin small">
</div> </div>
<div class="cell" v-show="isCarte"> <div class="cell" v-show="isCarte">
@ -130,6 +120,7 @@
:id="elementIdFor('quartiers_prioritaires')" :id="elementIdFor('quartiers_prioritaires')"
:name="nameFor('quartiers_prioritaires')" :name="nameFor('quartiers_prioritaires')"
v-model="options.quartiers_prioritaires" v-model="options.quartiers_prioritaires"
@change="update"
class="small-margin small" class="small-margin small"
value="1"> value="1">
Quartiers prioritaires Quartiers prioritaires
@ -141,6 +132,7 @@
:id="elementIdFor('cadastres')" :id="elementIdFor('cadastres')"
:name="nameFor('cadastres')" :name="nameFor('cadastres')"
v-model="options.cadastres" v-model="options.cadastres"
@change="update"
class="small-margin small" class="small-margin small"
value="1"> value="1">
Cadastres Cadastres
@ -152,6 +144,7 @@
:id="elementIdFor('parcelles_agricoles')" :id="elementIdFor('parcelles_agricoles')"
:name="nameFor('parcelles_agricoles')" :name="nameFor('parcelles_agricoles')"
v-model="options.parcelles_agricoles" v-model="options.parcelles_agricoles"
@change="update"
class="small-margin small" class="small-margin small"
value="1"> value="1">
Parcelles Agricoles Parcelles Agricoles
@ -163,7 +156,6 @@
<DraggableItem <DraggableItem
v-for="(item, index) in typesDeChamp" v-for="(item, index) in typesDeChamp"
:state="stateForRepetition" :state="stateForRepetition"
:update="update"
:index="index" :index="index"
:item="item" :item="item"
:key="item.id" /> :key="item.id" />
@ -181,7 +173,7 @@
</div> </div>
<div class="meta"> <div class="meta">
<input type="hidden" :name="nameFor('order_place')" :value="index"> <input type="hidden" :name="nameFor('order_place')" :value="index">
<input type="hidden" :name="nameFor('id')" :value="item.id"> <input type="hidden" :name="nameFor('id')" :value="id">
</div> </div>
</div> </div>
</template> </template>

View file

@ -1,13 +1,14 @@
export default { export default {
props: ['state', 'version', 'update', 'updateAll'], props: ['state', 'version'],
methods: { methods: {
addChamp() { addChamp() {
this.state.typesDeChamp.push({ this.state.typesDeChamp.push({
type_champ: 'text', type_champ: 'text',
drop_down_list: {}, types_de_champ: []
types_de_champ: [],
options: {}
}); });
},
save() {
this.state.flash.success();
} }
} }
}; };

View file

@ -10,14 +10,13 @@
</template> </template>
</button> </button>
<button class="button primary" @click.prevent="updateAll">Enregistrer</button> <button class="button primary" @click.prevent="save">Enregistrer</button>
</div> </div>
<Draggable :list="state.typesDeChamp" :options="{handle:'.handle'}"> <Draggable :list="state.typesDeChamp" :options="{handle:'.handle'}">
<DraggableItem <DraggableItem
v-for="(item, index) in state.typesDeChamp" v-for="(item, index) in state.typesDeChamp"
:state="state" :state="state"
:update="update"
:index="index" :index="index"
:item="item" :item="item"
:key="item.id" /> :key="item.id" />
@ -33,7 +32,7 @@
</template> </template>
</button> </button>
<button class="button primary" @click.prevent="updateAll">Enregistrer</button> <button class="button primary" @click.prevent="save">Enregistrer</button>
</div> </div>
</div> </div>
</template> </template>

View file

@ -1,6 +1,5 @@
import Vue from 'vue'; import Vue from 'vue';
import Draggable from 'vuedraggable'; import Draggable from 'vuedraggable';
import { fire, debounce } from '@utils';
import DraggableItem from './DraggableItem'; import DraggableItem from './DraggableItem';
import DraggableList from './DraggableList'; import DraggableList from './DraggableList';
@ -16,109 +15,74 @@ addEventListener('DOMContentLoaded', () => {
}); });
function initEditor(el) { function initEditor(el) {
const { directUploadsUrl, dragIconUrl } = el.dataset; const { directUploadUrl, dragIconUrl, saveUrl } = el.dataset;
const state = { const state = {
typesDeChamp: JSON.parse(el.dataset.typesDeChamp), typesDeChamp: JSON.parse(el.dataset.typesDeChamp),
typesDeChampOptions: JSON.parse(el.dataset.typesDeChampOptions), typesDeChampOptions: JSON.parse(el.dataset.typesDeChampOptions),
directUploadsUrl, directUploadUrl,
dragIconUrl, dragIconUrl,
saveUrl,
isAnnotation: el.dataset.type === 'annotation', isAnnotation: el.dataset.type === 'annotation',
unsavedItems: new Set(), prefix: 'procedure',
unsavedInvalidItems: new Set(), inFlight: 0,
version: 1, flash: new Flash()
prefix: 'procedure'
}; };
// 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({ new Vue({
el, el,
data: { data: {
state, state
update: null
}, },
render(h) { render(h) {
return h(DraggableList, { return h(DraggableList, {
props: { props: {
state: this.state, state: this.state
update: this.update,
updateAll: this.updateAll
} }
}); });
},
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) { class Flash {
let isSaving = false; constructor(isAnnotation) {
const form = app.$el.closest('form'); this.element = document.querySelector('#flash_messages');
this.isAnnotation = isAnnotation;
const update = ([id, isValid], refresh = true) => { }
app.state.unsavedItems.add(id); success() {
if (isValid) { if (this.isAnnotation) {
app.state.unsavedInvalidItems.delete(id); this.add('Annotations privées enregistrées.');
} else { } else {
app.state.unsavedInvalidItems.add(id); this.add('Formulaire enregistré.');
} }
if (refresh) { }
app.state.version += 1; error(message) {
} this.add(message, true);
updateAll(); }
}; clear() {
this.element.innerHTML = '';
}
add(message, isError) {
const html = `<div id="flash_message" class="center">
<div class="alert alert-fixed ${
isError ? 'alert-danger' : 'alert-success'
}">
${message}
</div>
</div>`;
const updateAll = debounce(() => { this.element.innerHTML = html;
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);
addEventListener('ProcedureUpdated', event => { setTimeout(() => {
const { types_de_champ, types_de_champ_private } = event.detail; this.clear();
}, 6000);
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');
} }
} }

View file

@ -38,13 +38,26 @@ function source(url) {
} }
addEventListener('turbolinks:load', function() { addEventListener('turbolinks:load', function() {
autocompleteSetup();
});
addEventListener('ajax:success', function() {
autocompleteSetup();
});
function autocompleteSetup() {
for (let { type, url } of sources) { for (let { type, url } of sources) {
for (let target of document.querySelectorAll(selector(type))) { for (let element of document.querySelectorAll(selector(type))) {
let select = autocomplete(target, options, [source(url)]); element.removeAttribute('data-autocomplete');
select.on('autocomplete:selected', ({ target }, suggestion) => { autocompleteInitializeElement(element, url);
fire(target, 'autocomplete:select', suggestion);
select.autocomplete.setVal(suggestion.label);
});
} }
} }
}); }
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);
});
}

View file

@ -15,7 +15,7 @@ class ApiCarto::API
params = geojson.to_s params = geojson.to_s
RestClient.post(url, params, content_type: 'application/json') 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}" Rails.logger.error "[ApiCarto] Error on #{url}: #{e}"
raise RestClient::ResourceNotFound raise RestClient::ResourceNotFound
end end

View file

@ -120,17 +120,11 @@ class Dossier < ApplicationRecord
end end
def build_default_champs def build_default_champs
procedure.types_de_champ.each do |type_de_champ| procedure.build_champs.each do |champ|
champ = type_de_champ.champ.build
if type_de_champ.repetition?
champ.add_row
end
champs << champ champs << champ
end end
procedure.types_de_champ_private.each do |type_de_champ| procedure.build_champs_private.each do |champ|
champs_private << type_de_champ.champ.build champs_private << champ
end end
end end
@ -166,7 +160,11 @@ class Dossier < ApplicationRecord
!procedure.archivee? && brouillon? !procedure.archivee? && brouillon?
end 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? brouillon? || en_construction?
end end

View file

@ -138,10 +138,15 @@ class Procedure < ApplicationRecord
# Warning: dossier after_save build_default_champs must be removed # Warning: dossier after_save build_default_champs must be removed
# to save a dossier created from this method # to save a dossier created from this method
def new_dossier def new_dossier
champs = types_de_champ.map { |tdc| tdc.champ.build } Dossier.new(procedure: self, champs: build_champs, champs_private: build_champs_private)
champs_private = types_de_champ_private.map { |tdc| tdc.champ.build } 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 end
def default_path def default_path

View file

@ -120,6 +120,10 @@ class TypeDeChamp < ApplicationRecord
} }
end end
def build_champ
dynamic_type.build_champ
end
def self.type_de_champs_list_fr def self.type_de_champs_list_fr
type_champs.map { |champ| [I18n.t("activerecord.attributes.type_de_champ.type_champs.#{champ.last}"), champ.first] } type_champs.map { |champ| [I18n.t("activerecord.attributes.type_de_champ.type_champs.#{champ.last}"), champ.first] }
end end
@ -175,6 +179,14 @@ class TypeDeChamp < ApplicationRecord
end end
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 private
def setup_procedure def setup_procedure

View file

@ -1,2 +1,7 @@
class TypesDeChamp::RepetitionTypeDeChamp < TypesDeChamp::TypeDeChampBase class TypesDeChamp::RepetitionTypeDeChamp < TypesDeChamp::TypeDeChampBase
def build_champ
champ = super
champ.add_row
champ
end
end end

View file

@ -19,4 +19,8 @@ class TypesDeChamp::TypeDeChampBase
} }
] ]
end end
def build_champ
@type_de_champ.champ.build
end
end end

View file

@ -1,3 +0,0 @@
<%= render_flash timeout: 6000, fixed: true %>
<%= fire_event(:ProcedureUpdated, procedure_data(@procedure.reload)) %>

View file

@ -51,7 +51,7 @@
= link_to(url_for_dossier(dossier), class: 'cell-link') do = link_to(url_for_dossier(dossier), class: 'cell-link') do
= dossier.updated_at.strftime("%d/%m/%Y") = dossier.updated_at.strftime("%d/%m/%Y")
%td.action-col.delete-col %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 quil contient. Toute suppression entraine lannulation de la démarche en cours.\n\nConfirmer la suppression ?" }) do = 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 quil contient. Toute suppression entraine lannulation de la démarche en cours.\n\nConfirmer la suppression ?" }) do
%span.icon.delete %span.icon.delete
Supprimer Supprimer

View file

@ -5,11 +5,16 @@
= form.fields_for :champs, champ do |form| = form.fields_for :champs, champ do |form|
= render partial: 'shared/dossiers/editable_champs/editable_champ', locals: { champ: form.object, form: form } = render partial: 'shared/dossiers/editable_champs/editable_champ', locals: { champ: form.object, form: form }
= form.hidden_field :_destroy, disabled: true = form.hidden_field :_destroy, disabled: true
%button.button.danger.remove-row .flex.row-reverse
Supprimer - if champ.persisted?
%button.button.danger.remove-row
Supprimer
- else
%button.button.danger{ type: :button }
Supprimer
- if champ.persisted? - 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 } = 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 - else
%button.button.add-row{ disabled: true } %button.button{ type: :button }
= "Ajouter une ligne pour « #{champ.libelle} »" = "Ajouter une ligne pour « #{champ.libelle} »"

View file

@ -367,6 +367,8 @@ Rails.application.routes.draw do
get 'champs' get 'champs'
get 'annotations' get 'annotations'
end end
resources :types_de_champ, only: [:create, :update, :destroy]
end end
resources :services, except: [:show] do resources :services, except: [:show] do

View file

@ -798,13 +798,13 @@ describe NewUser::DossiersController, type: :controller do
subject { post :ask_deletion, params: { id: dossier.id } } subject { post :ask_deletion, params: { id: dossier.id } }
shared_examples_for "the dossier can not be deleted" do shared_examples_for "the dossier can not be deleted" do
it do it "doesnt notify the deletion" do
expect(DossierMailer).not_to receive(:notify_deletion_to_administration) expect(DossierMailer).not_to receive(:notify_deletion_to_administration)
expect(DossierMailer).not_to receive(:notify_deletion_to_user) expect(DossierMailer).not_to receive(:notify_deletion_to_user)
subject subject
end end
it do it "doesnt delete the dossier" do
subject subject
expect(Dossier.find_by(id: dossier.id)).not_to eq(nil) expect(Dossier.find_by(id: dossier.id)).not_to eq(nil)
expect(dossier.procedure.deleted_dossiers.count).to eq(0) 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 context 'when dossier is owned by signed in user' do
let(:dossier) { create(:dossier, :en_construction, user: user, autorisation_donnees: true) } 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_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)) expect(DossierMailer).to receive(:notify_deletion_to_user).with(kind_of(DeletedDossier), dossier.user.email).and_return(double(deliver_later: nil))
subject subject
end end
it do it "deletes the dossier" do
procedure = dossier.procedure procedure = dossier.procedure
dossier_id = dossier.id dossier_id = dossier.id
subject subject

View file

@ -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') 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' 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 within '.footer' do
click_on 'Ajouter un champ' click_on 'Ajouter un champ'
@ -129,7 +130,8 @@ feature 'As an administrateur I wanna create a new procedure', js: true do
page.refresh page.refresh
fill_in 'procedure_types_de_champ_attributes_0_libelle', with: 'libelle de champ' 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 Procedure.last.libelle
click_on 'onglet-pieces' click_on 'onglet-pieces'

View file

@ -1,6 +1,6 @@
require 'spec_helper' 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(:administrateur) { procedure.administrateur }
let(:procedure) { create(:procedure) } let(:procedure) { create(:procedure) }
@ -18,14 +18,15 @@ feature 'As an administrateur I edit procedure', js: true do
end end
expect(page).to have_selector('#procedure_types_de_champ_attributes_0_libelle') 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' 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 page.refresh
within '.footer' do within '.footer' do
click_on 'Enregistrer' click_on 'Enregistrer'
end end
expect(page).to have_content('Formulaire mis à jour') expect(page).to have_content('Formulaire enregistré')
end end
it "Add multiple champs" do 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'
click_on 'Ajouter un champ' click_on 'Ajouter un champ'
end end
expect(page).not_to have_content('Le libellé doit être rempli.') expect(page).not_to have_content('Formulaire enregistré')
expect(page).not_to have_content('Modifications non sauvegardées.')
expect(page).not_to have_content('Formulaire mis à jour')
fill_in 'procedure_types_de_champ_attributes_0_libelle', with: 'libellé de champ 0' 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_0_libelle')
expect(page).to have_selector('#procedure_types_de_champ_attributes_1_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_2_libelle')
expect(page).to have_selector('#procedure_types_de_champ_attributes_3_libelle') expect(page).to have_selector('#procedure_types_de_champ_attributes_3_libelle')
expect(page).to have_content('Le libellé doit être rempli.') within '.draggable-item-2' do
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
click_on 'Supprimer' click_on 'Supprimer'
end end
expect(page).to have_content('Le libellé doit être rempli.') expect(page).not_to have_selector('#procedure_types_de_champ_attributes_3_libelle')
expect(page).to have_content('Modifications non sauvegardées.') fill_in 'procedure_types_de_champ_attributes_2_libelle', with: 'libellé de champ 2'
expect(page).not_to have_content('Formulaire mis à jour') blur
fill_in 'procedure_types_de_champ_attributes_1_libelle', with: 'libellé de champ 1' 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 page.refresh
expect(page).to have_content('Supprimer', count: 3) 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 it "Remove champs" do
fill_in 'procedure_types_de_champ_attributes_0_libelle', with: 'libellé de champ' 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 page.refresh
click_on 'Supprimer' 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') expect(page).not_to have_content('Supprimer')
page.refresh page.refresh
@ -82,19 +80,21 @@ feature 'As an administrateur I edit procedure', js: true do
it "Only add valid champs" do it "Only add valid champs" do
expect(page).to have_selector('#procedure_types_de_champ_attributes_0_description') 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' fill_in 'procedure_types_de_champ_attributes_0_description', with: 'déscription du champ'
expect(page).to have_content('Le libellé doit être rempli.') blur
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' 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 end
it "Add repetition champ" do it "Add repetition champ" do
expect(page).to have_selector('#procedure_types_de_champ_attributes_0_libelle') 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') 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' 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 page.refresh
within '.flex-grow' do within '.flex-grow' do
@ -102,8 +102,9 @@ feature 'As an administrateur I edit procedure', js: true do
end end
fill_in 'procedure_types_de_champ_attributes_0_types_de_champ_attributes_0_libelle', with: 'libellé de champ 1' 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) expect(page).to have_content('Supprimer', count: 2)
within '.footer' do 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') 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' fill_in 'procedure_types_de_champ_attributes_1_libelle', with: 'libellé de champ 2'
blur
expect(page).to have_content('Supprimer', count: 3) expect(page).to have_content('Supprimer', count: 3)
end end

View file

@ -1,13 +1,13 @@
require 'spec_helper' 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(:user) { create(:user) }
let!(:last_updated_dossier) { create(:dossier, :with_entreprise, user: user, state: Dossier.states.fetch(:en_construction)) } let!(:dossier_brouillon) { create(:dossier, user: user) }
let!(:dossier1) { create(:dossier, :with_entreprise, user: user, state: Dossier.states.fetch(:en_construction)) } let!(:dossier_en_construction) { create(:dossier, :en_construction, user: user) }
let!(:dossier2) { create(:dossier, :with_entreprise) } let!(:dossier_en_instruction) { create(:dossier, :en_instruction, user: user) }
let!(:dossier_brouillon) { create(:dossier, :with_entreprise, user: user) } let!(:dossier_archived) { create(:dossier, :en_instruction, :archived, user: user) }
let!(:dossier_archived) { create(:dossier, :with_entreprise, user: user, state: Dossier.states.fetch(:en_construction)) }
let(:dossiers_per_page) { 25 } let(:dossiers_per_page) { 25 }
let(:last_updated_dossier) { dossier_en_construction }
before do before do
@default_per_page = Dossier.default_per_page @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) last_updated_dossier.update_column(:updated_at, "19/07/2052 15:35".to_time)
visit new_user_session_path login_as user, scope: :user
within('#new_user') do visit dossiers_path
page.find_by_id('user_email').set user.email
page.find_by_id('user_password').set user.password
page.click_on 'Se connecter'
end
end end
after do after do
@ -28,52 +24,59 @@ describe 'user access to the list of his dossier' do
end end
it 'the list of dossier is displayed' do it 'the list of dossier is displayed' do
expect(page).to have_content(dossier1.procedure.libelle) expect(page).to have_content(dossier_brouillon.procedure.libelle)
expect(page).to have_content('en construction') expect(page).to have_content(dossier_en_construction.procedure.libelle)
end expect(page).to have_content(dossier_en_instruction.procedure.libelle)
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_archived.procedure.libelle) expect(page).to have_content(dossier_archived.procedure.libelle)
end end
it 'should have link to only delete brouillon' do it 'the list must be ordered by last updated' do
expect(page).to have_link(nil, href: ask_deletion_dossier_path(dossier_brouillon)) expect(page.body).to match(/#{last_updated_dossier.procedure.libelle}.*#{dossier_en_instruction.procedure.libelle}/m)
expect(page).not_to have_link(nil, href: ask_deletion_dossier_path(dossier1))
end end
context 'when user clicks on delete brouillon', js: true do context 'when there are dossiers from other users' do
scenario 'dossier is deleted' do let!(:dossier_other_user) { create(:dossier) }
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é.') it 'doesnt display dossiers belonging to other users' do
end expect(page).not_to have_content(dossier_other_user.procedure.libelle)
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))
end end
end end
context 'when there is more than one page' do context 'when there is more than one page' do
let(:dossiers_per_page) { 2 } 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") 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
end end
@ -91,25 +94,27 @@ describe 'user access to the list of his dossier' do
end end
context "when the dossier does not belong to the user" do context "when the dossier does not belong to the user" do
let!(:dossier_other_user) { create(:dossier) }
before do 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") click_button("Rechercher")
end end
it "shows an error message on the dossiers page" do it "shows an error message on the dossiers page" do
expect(current_path).to eq(dossiers_path) expect(current_path).to eq(dossiers_path)
expect(page).to have_content("Vous navez pas de dossier avec le nº #{dossier2.id}.") expect(page).to have_content("Vous navez pas de dossier avec le nº #{dossier_other_user.id}.")
end end
end end
context "when the dossier belongs to the user" do context "when the dossier belongs to the user" do
before 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") click_button("Rechercher")
end end
it "redirects to the dossier page" do 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 end
end end

View file

@ -58,6 +58,10 @@ module FeatureHelpers
# Procedure contact infos in the footer # Procedure contact infos in the footer
expect(page).to have_content(procedure.service.email) expect(page).to have_content(procedure.service.email)
end end
def blur
page.find('body').click
end
end end
RSpec.configure do |config| RSpec.configure do |config|