commit
0b58bed658
27 changed files with 480 additions and 336 deletions
|
@ -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.
|
||||
|
|
|
@ -32,6 +32,10 @@
|
|||
&.column {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
&.row-reverse {
|
||||
flex-direction: row-reverse;
|
||||
}
|
||||
}
|
||||
|
||||
.flex-grow {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<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">
|
||||
</div>
|
||||
|
||||
|
@ -14,6 +14,7 @@
|
|||
:id="elementIdFor('type_champ')"
|
||||
:name="nameFor('type_champ')"
|
||||
v-model="typeChamp"
|
||||
@change="update"
|
||||
class="small-margin small inline">
|
||||
<option v-for="option in state.typesDeChampOptions" :key="option[1]" :value="option[1]">
|
||||
{{ option[0] }}
|
||||
|
@ -21,21 +22,7 @@
|
|||
</select>
|
||||
</div>
|
||||
<div class="flex justify-start delete">
|
||||
<div v-if="isDirty" class="error-message">
|
||||
<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)">
|
||||
<button class="button danger" @click.prevent="removeChamp">
|
||||
Supprimer
|
||||
</button>
|
||||
</div>
|
||||
|
@ -51,8 +38,9 @@
|
|||
:id="elementIdFor('libelle')"
|
||||
:name="nameFor('libelle')"
|
||||
v-model="libelle"
|
||||
@change="update"
|
||||
class="small-margin small"
|
||||
:class="{ error: isDirty && isInvalid }">
|
||||
:class="{ error: hasChanges && !isValid }">
|
||||
</div>
|
||||
|
||||
<div class="cell" v-show="!isHeaderSection && !isExplication && !state.isAnnotation">
|
||||
|
@ -65,6 +53,7 @@
|
|||
:id="elementIdFor('mandatory')"
|
||||
:name="nameFor('mandatory')"
|
||||
v-model="mandatory"
|
||||
@change="update"
|
||||
class="small-margin small"
|
||||
value="1">
|
||||
</div>
|
||||
|
@ -78,6 +67,7 @@
|
|||
:id="elementIdFor('description')"
|
||||
:name="nameFor('description')"
|
||||
v-model="description"
|
||||
@change="update"
|
||||
rows=3
|
||||
cols=40
|
||||
class="small-margin small">
|
||||
|
@ -93,7 +83,8 @@
|
|||
<textarea
|
||||
:id="elementIdFor('drop_down_list')"
|
||||
:name="nameFor('drop_down_list_attributes[value]')"
|
||||
v-model="dropDownList"
|
||||
v-model="dropDownListValue"
|
||||
@change="update"
|
||||
rows=3
|
||||
cols=40
|
||||
placeholder="Ecrire une valeur par ligne et --valeur-- pour un séparateur."
|
||||
|
@ -104,9 +95,9 @@
|
|||
<label :for="elementIdFor('piece_justificative_template')">
|
||||
Modèle
|
||||
</label>
|
||||
<template v-if="item.piece_justificative_template_url">
|
||||
<a :href="item.piece_justificative_template_url" target="_blank">
|
||||
{{item.piece_justificative_template_filename}}
|
||||
<template v-if="pieceJustificativeTemplateUrl">
|
||||
<a :href="pieceJustificativeTemplateUrl" target="_blank">
|
||||
{{pieceJustificativeTemplateFilename}}
|
||||
</a>
|
||||
<br> Modifier :
|
||||
</template>
|
||||
|
@ -114,8 +105,7 @@
|
|||
type="file"
|
||||
:id="elementIdFor('piece_justificative_template')"
|
||||
:name="nameFor('piece_justificative_template')"
|
||||
:data-direct-upload-url="state.directUploadsUrl"
|
||||
@change="update(changeLog)"
|
||||
@change="upload"
|
||||
class="small-margin small">
|
||||
</div>
|
||||
<div class="cell" v-show="isCarte">
|
||||
|
@ -130,6 +120,7 @@
|
|||
:id="elementIdFor('quartiers_prioritaires')"
|
||||
:name="nameFor('quartiers_prioritaires')"
|
||||
v-model="options.quartiers_prioritaires"
|
||||
@change="update"
|
||||
class="small-margin small"
|
||||
value="1">
|
||||
Quartiers prioritaires
|
||||
|
@ -141,6 +132,7 @@
|
|||
:id="elementIdFor('cadastres')"
|
||||
:name="nameFor('cadastres')"
|
||||
v-model="options.cadastres"
|
||||
@change="update"
|
||||
class="small-margin small"
|
||||
value="1">
|
||||
Cadastres
|
||||
|
@ -152,6 +144,7 @@
|
|||
:id="elementIdFor('parcelles_agricoles')"
|
||||
:name="nameFor('parcelles_agricoles')"
|
||||
v-model="options.parcelles_agricoles"
|
||||
@change="update"
|
||||
class="small-margin small"
|
||||
value="1">
|
||||
Parcelles Agricoles
|
||||
|
@ -163,7 +156,6 @@
|
|||
<DraggableItem
|
||||
v-for="(item, index) in typesDeChamp"
|
||||
:state="stateForRepetition"
|
||||
:update="update"
|
||||
:index="index"
|
||||
:item="item"
|
||||
:key="item.id" />
|
||||
|
@ -181,7 +173,7 @@
|
|||
</div>
|
||||
<div class="meta">
|
||||
<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>
|
||||
</template>
|
||||
|
|
|
@ -1,13 +1,14 @@
|
|||
export default {
|
||||
props: ['state', 'version', 'update', 'updateAll'],
|
||||
props: ['state', 'version'],
|
||||
methods: {
|
||||
addChamp() {
|
||||
this.state.typesDeChamp.push({
|
||||
type_champ: 'text',
|
||||
drop_down_list: {},
|
||||
types_de_champ: [],
|
||||
options: {}
|
||||
types_de_champ: []
|
||||
});
|
||||
},
|
||||
save() {
|
||||
this.state.flash.success();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
@ -10,14 +10,13 @@
|
|||
</template>
|
||||
</button>
|
||||
|
||||
<button class="button primary" @click.prevent="updateAll">Enregistrer</button>
|
||||
<button class="button primary" @click.prevent="save">Enregistrer</button>
|
||||
</div>
|
||||
|
||||
<Draggable :list="state.typesDeChamp" :options="{handle:'.handle'}">
|
||||
<DraggableItem
|
||||
v-for="(item, index) in state.typesDeChamp"
|
||||
:state="state"
|
||||
:update="update"
|
||||
:index="index"
|
||||
:item="item"
|
||||
:key="item.id" />
|
||||
|
@ -33,7 +32,7 @@
|
|||
</template>
|
||||
</button>
|
||||
|
||||
<button class="button primary" @click.prevent="updateAll">Enregistrer</button>
|
||||
<button class="button primary" @click.prevent="save">Enregistrer</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -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()
|
||||
};
|
||||
|
||||
new Vue({
|
||||
el,
|
||||
data: {
|
||||
state,
|
||||
update: null
|
||||
},
|
||||
render(h) {
|
||||
return h(DraggableList, {
|
||||
props: {
|
||||
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({
|
||||
if (state.typesDeChamp.length === 0) {
|
||||
state.typesDeChamp.push({
|
||||
type_champ: 'text',
|
||||
types_de_champ: []
|
||||
});
|
||||
}
|
||||
|
||||
new Vue({
|
||||
el,
|
||||
data: {
|
||||
state
|
||||
},
|
||||
render(h) {
|
||||
return h(DraggableList, {
|
||||
props: {
|
||||
state: this.state
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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();
|
||||
};
|
||||
|
||||
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');
|
||||
error(message) {
|
||||
this.add(message, true);
|
||||
}
|
||||
}, 500);
|
||||
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>`;
|
||||
|
||||
addEventListener('ProcedureUpdated', event => {
|
||||
const { types_de_champ, types_de_champ_private } = event.detail;
|
||||
this.element.innerHTML = html;
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)]);
|
||||
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);
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -1,2 +1,7 @@
|
|||
class TypesDeChamp::RepetitionTypeDeChamp < TypesDeChamp::TypeDeChampBase
|
||||
def build_champ
|
||||
champ = super
|
||||
champ.add_row
|
||||
champ
|
||||
end
|
||||
end
|
||||
|
|
|
@ -19,4 +19,8 @@ class TypesDeChamp::TypeDeChampBase
|
|||
}
|
||||
]
|
||||
end
|
||||
|
||||
def build_champ
|
||||
@type_de_champ.champ.build
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,3 +0,0 @@
|
|||
<%= render_flash timeout: 6000, fixed: true %>
|
||||
|
||||
<%= fire_event(:ProcedureUpdated, procedure_data(@procedure.reload)) %>
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
.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} »"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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|
|
||||
|
|
Loading…
Reference in a new issue