...
Dans "Admin" puis "Circuit", vous avez accès à l'outil permet de consulter et de modifier les circuits de signatures. Il est possible de filtrer les circuits présents dans esup-signature :
- Workflows globaux (définis pas les administrateurs)
- Classes workflow (définies par les développeurs)
- Workflows Utilisateurs (définis pas les utilisateurs)
Création d'un circuit classique
...
Dans cette vue il est possible d'activer la notion d'étape "infinie" en activant "L'utilisateur peut ajouter une étape avant la suivante". Si cette option est activée, vous donnerez la possibilité aux participants de cette étape, d'ajouter des étapes intermédiaires (non prévues à l'origine).
Chaque étape créée à la suite d'une étape "infinie" sera elle aussi une étape "infini". Cela se traduit, au niveau de l'interface utilisateur, par une question qui lui est posée lorsque qu'il s’apprête à signer : "Signer et passer à l'étape suivante" ou "signer et ajouter une étape"
Paramètres généraux d'un circuit
...
[TextField]^(SIG_SIGNATURE|SIG_CACHET)$ : pour détecter les champs de type TextField avec les attributs "nom" ("name") SIG_SIGNATURE ou SIG_CACHET
...
| Type | Chemin | Adresse à saisir | Remarques |
|---|---|---|---|
| SMB | Absolu | smb://<adresse du serveur>/<chemin du dossier> | smb-test-uri à configurer obligatoirement pour activer la fonctionnalité. L'utilisateur est configuré dans application.yml (smb-login, smb-password) |
| CMIS | Relatif | cmis://<chemin du dossier> ex: cmis://default-domain/workspaces/test | Il faut configurer l'adresse du server nuxeo dans application.yml au niveau de (cmis-test-uri ex: http://mon-nuxeo.univ-ville.fr:8081/nuxeo, cmis-login, cmis-password) Cette partie est amenée à changer prochainement pour permettre les chemins absolus |
| FILE | Absolu | /<chemin du dossier> | vfs-test-urià configurer obligatoirement pour activer la fonctionnalité. |
| FTP / SFTP | Absolu | ftp://<user>:<password>@<adresse du serveur>/<chemin du dossier> | vfs-test-uri à configurer obligatoirement pour activer la fonctionnalité. |
| Remarque |
|---|
Lorsqu'un document est intégré de cette façon, il est supprimé du dossier source. |
...
- smb, cmis, file, sftp / ftp, pour ces types, il faut suive les explications du tableau ci-dessus
- http:// https:// (fera une requete, type REST, vers l'url avec en paramètre l'ID de la demande de signature, son statut signé ou refusé et le numéro d'étape concerné
- mailto (envoi du document en pièce jointe au destinataire)
Transmission vers REST
(maj depuis pour la version 1.30.x)
Lorsque vous configurez une adresse type "rest", donc vers un websevice en écoute, esup-signature va tenté de transmettre les données via GET et via POST.
Voici à quels moments les requettes sont transmisent :
- À chaque signature / refus (avec le motif)
- À la suppression / suppression définitive
- À la restautation
- À la fin du circuit
Les statuts transmis sont les suivants : pending, refused, deleted, restored, cleaned, completed
Voici la liste (et le format) des données transmisent :
| Bloc de code | ||||
|---|---|---|---|---|
| ||||
{
"signRequestId": "XXXX",
"status": "pending",
"step": "1",
"userEppn": "toto@univ-ville.fr",
"comment": ""
}
|
En GET les informations sont transmisent via les paramètres de la requête.
Voici un exemple de code à mettre coté application métier pour permettre à esup-signature de transmettre les informations du circuit via REST. Ici il s'agir d'un controleur Spring. La requête est envoyée en GET, esup-signature doit avoir un accès direct à cette API.
| Bloc de code | ||||
|---|---|---|---|---|
| ||||
@GetMapping(value = "/return-test")
@ResponseBody
public ResponseEntity<Void> returnTest(@RequestParam("signRequestId") String signRequestId, @RequestParam("status") String status, @RequestParam("step") String step) {
logger.info(signRequestId + ", " + status + ", " + step);
//ici, le code à executer coté application métier en fonction du statut
return ResponseEntity.ok().build();
}
|
En POST, les informations sont transmisent sous forme de map json:
Voici un script python permettant de simuler un mini server web et affichant les données reçues ici en http://localhost:8000 :
Verification du bon fonctionnement de la copie coté serveur avec smbclient :
| Bloc de code | ||
|---|---|---|
| ||
apt install smbclient
echo "Contenu de test" > /tmp/test_fichier.txt
smbclient //stockage.univ-ville.fr/nom_du_partage -W MON_WORKGROUP -U mon_uid -c "put /tmp/test_fichier.txt mon_du_dossier/test_fichier.txt" |
La copie doit fonctionner pour qu'esup-signature puisse en faire autant.
Transmission vers REST
(maj depuis pour la version 1.30.x)
Lorsque vous configurez une adresse type "rest", donc vers un websevice en écoute, esup-signature va tenté de transmettre les données via GET et via POST.
Voici à quels moments les requettes sont transmisent :
- À chaque signature / refus (avec le motif)
- À la suppression / suppression définitive
- À la restautation
- À la fin du circuit
Les statuts transmis sont les suivants : pending, refused, deleted, restored, cleaned, completed
Voici la liste (et le format) des données transmisent :
| Bloc de code | ||||
|---|---|---|---|---|
| ||||
{
"signRequestId": "XXXX",
"status": "pending",
"step": "1",
"userEppn": "toto@univ-ville.fr",
"comment": ""
}
|
En GET les informations sont transmisent via les paramètres de la requête.
Voici un exemple de code à mettre coté application métier pour permettre à esup-signature de transmettre les informations du circuit via REST. Ici il s'agir d'un controleur Spring. La requête est envoyée en GET, esup-signature doit avoir un accès direct à cette API.
| Bloc de code | ||||
|---|---|---|---|---|
| ||||
@GetMapping(value = "/return-test")
@ResponseBody
public ResponseEntity<Void> returnTest(@RequestParam("signRequestId") String signRequestId, @RequestParam("status") String status, @RequestParam("step") String step) {
logger.info(signRequestId + ", " + status + ", " + step); | ||||
| Bloc de code | ||||
| ||||
from http.server import BaseHTTPRequestHandler, HTTPServer from urllib.parse import urlparse, parse_qs import json class SimpleHTTPRequestHandler(BaseHTTPRequestHandler): def do_GET(self): # Parse l'URL et ses paramètres parsed_path = urlparse(self.path) query_components = parse_qs(parsed_path.query)//ici, le code à executer coté application métier en fonction du statut return ResponseEntity.ok().build(); } |
En POST, les informations sont transmisent sous forme de map json:
Voici un script python permettant de simuler un mini server web et affichant les données reçues ici en http://localhost:8000 :
| Bloc de code | ||||
|---|---|---|---|---|
| ||||
from http.server import BaseHTTPRequestHandler, HTTPServer from urllib.parse import urlparse, parse_qs import json class SimpleHTTPRequestHandler(BaseHTTPRequestHandler): # Affiche les informations dans la console print("Received GET request:") print(f"Path: {self.path}")def do_GET(self): print("Query parameters:") # Parse l'URL et ses paramètres for key, values in query_components.items():parsed_path = urlparse(self.path) query_components for value in values: = parse_qs(parsed_path.query) # Affiche les print(f"informations dans {key}: {value}")la console print("Received GET request:") # Prépare la réponse (simple pour le clientprint(f"Path: {self.path}") self.send_response(200print("Query parameters:") self.send_header('Content-type', 'text/html') for key, values in query_components.items(): for self.end_headers()value in values: self.wfile.write(b"<html><body><h1>GET request received</h1></body></html>") print(f" {key}: {value}") def do_POST(self): # RécupèrePrépare la longueur des données envoyéesréponse (simple pour le client) content_length = int(self.headers['Content-Length']self.send_response(200) # Récupère le corps de la requête self.send_header('Content-type', 'text/html') self.end_headers() post_data = self.rfilewfile.read(content_length) write(b"<html><body><h1>GET request received</h1></body></html>") def do_POST(self): # TenteRécupère dela décoderlongueur lesdes données comme JSON envoyées content_length = int(self.headers['Content-Length']) try: # Récupère le corps de la requête jsonpost_data = jsonself.rfile.loadsread(postcontent_datalength) print("Received POST request with# JSON data:")Tente de décoder les données comme JSON try: print(json.dumps(json_data, indent=4)) json_data except= json.JSONDecodeError:loads(post_data) print("Received POST request with invalidJSON JSONdata:") print(json.dumps(json_data, indent= {"error": "Invalid JSON"4)) except json.JSONDecodeError: print("Received POST request with invalid JSON") json_data = {"error": "Invalid JSON"} # Prépare la réponse (simple pour le client) self.send_response(200) self.send_header('Content-type', 'text/html') self.end_headers() self.wfile.write(b"<html><body><h1>POST request received</h1></body></html>") def run(server_class=HTTPServer, handler_class=SimpleHTTPRequestHandler, port=8000): server_address = ('', port) httpd = server_class(server_address, handler_class) print(f"Starting server on port {port}...") httpd.serve_forever() if __name__ == '__main__': run() |
...
Mettre en place un script qui va calculer le workflow, utiliser les web-services pour configurer les participants du circuit des bons de commandes et injecter le document. (pour plus de détails sur les web services voir : Web services REST)
Mettre en place un script qui calcule le workflow, inscrit ce workflow dans les métas-données du document (PDF) et le copie dans un dossier défini comme "source" au niveau d'Esup-signature.
Pour cette dernière solution il faut donc créer un circuit comme vu précédemment, cocher la case "Scanner les métadonnées des PDF" et définir une source pour la récupération des documents au niveau des paramètres générauxImplémenter une classe worflow (voir le chapitre suivant)
Les métas-données qui doivent être inscrite dans les document PDF sont les suivantes:
...
| Bloc de code | ||||
|---|---|---|---|---|
| ||||
PDDocument document = PDDocument.load(in);
PDDocumentInformation info = document.getDocumentInformation();
info.setCustomMetadataValue("sign_type_default_val", "pdfImageStamp");
info.setCustomMetadataValue("sign_step#1", "[machin@univ-ville.fr, truc@univ-ville.fr]");
info.setCustomMetadataValue("sign_target_key","smb://serveur_de_fichiers/la_destination/signed");
|
...
Utiliser les classes workflow
| Remarque |
|---|
Le système des classes "workflow" a été revu en version 1.36.x. Le fonctionnement des anciennes versions n'est plus assuré. Une refonte a été effectuée car l'ancien système n'apportait pas les fonctionnalités voulues. La nouvelle version est maintenant bien execuée à chaque nouvelle instance d'un workflow. De plus, ces workflows sont maintenant accessible en configuration (options et autorisations d'accès) au niveau de l'interface admin. |
Pour les cas les plus spécifiques, il est possible d'ajouter, au code source Pour les cas les plus spécifiques, il est possible d'ajouter, au code source original d'esup-signature, une classe qui décrira précisément construira un circuit de manière dynamique via du code java.
...
L’intérêt de cette solution est de
...
permettre la mise en place
...
de mécanismes complexes de calcul des participants et des paramètres des étapes. On peut par exemple imaginer de calculer le n+1 de l'utilisateur courant. Cela nécessite d'avoir des compétences en développement et connaitre notamment le langage Java et le framework Spring
Une nouvelle classe workflow devra étendre "ClassWorkflow" et implémenter la classe "DefaultWorkflowModelClassWorkflow". Des exemples sont déjà présents dans le code source original d'Esup-signature dans le dossier srcdossier src/main/java/org/esupportail/esupsignature/service/workflow/impl/
...
| Bloc de code | ||||
|---|---|---|---|---|
| ||||
package org.esupportail.esupsignature.service.interfaces.workflow.impl; import org.esupportail.esupsignature.entitydto.json.DataWorkflowStepDto; import org.esupportail.esupsignature.entity.UserWorkflowStep; import org.esupportail.esupsignature.entity.WorkflowStepenums.SignType; import org.esupportail.esupsignature.entity.enums.SignTypeexception.EsupSignatureUserException; import org.esupportail.esupsignature.service.UserService; import org.esupportail.esupsignature.exception.EsupSignatureUserExceptionservice.interfaces.workflow.ClassWorkflow; import org.esupportail.esupsignature.service.interfaces.workflow.DefaultWorkflowModelClassWorkflow; import org.springframework.stereotype.Component; import java.util.ArrayList; import java.util.List; @Component public class BasicWorkflowCreatorAndManagerClassWorkflow extends ClassWorkflow implements DefaultWorkflowModelClassWorkflow { private Stringtransient namefinal =UserService "BasicWorkflow"userService; private Stringpublic description = "Une signature"CreatorAndManagerClassWorkflow(UserService userService) { this.userService = userService; private List<WorkflowStep> workflowSteps;} @Override public String getName() { return name"CreatorAndManagerClassWorkflow"; } @Override public String getDescription() { return description "Signature du créateur puis du responsable"; } @Override public List<WorkflowStep> getWorkflowSteps() { if(this.workflowSteps == null) { return generateWorkflowSteps("creator", null); } @Override try { public List<WorkflowStep> generateWorkflowSteps(String userEppn, List<WorkflowStepDto> workflowStepDto) throws EsupSignatureUserException { List<WorkflowStep> this.workflowSteps = generateWorkflowSteps(userService.getCurrentUser(), null, nullnew ArrayList<>(); } catch (EsupSignatureUserException e) {//STEP 1 WorkflowStep workflowStep1 = return nullnew WorkflowStep(); }workflowStep1.getUsers().add(userService.getCreatorUser()); } workflowStep1.setDescription("Votre signature"); return this.workflowSteps workflowStep1.setSignType(SignType.signature); } public void initWorkflowStepsworkflowSteps.add(workflowStep1) {; this.workflowSteps = new ArrayList<>();//STEP 2 } @Override WorkflowStep publicworkflowStep2 = List<WorkflowStep>new generateWorkflowSteps(User user, Data data, List<String> recipentEmailsStep) throws EsupSignatureUserException { WorkflowStep(); List<WorkflowStep> workflowSteps = new ArrayList<>( workflowStep2.setSignType(SignType.signature); /* ici on construit la liste des étapes du circuit */ WorkflowStep workflowStep = new WorkflowStep( workflowStep2.setDescription("Signature de votre supérieur hiérarchique (présélectionné en fonction de vos précédentes saisies)"); workflowStep.setStepNumber(1 workflowStep2.getUsers().add(userService.getGenericUser()); workflowStep.setSignType(SignType.pdfImageStamp); //à remplacer par workflowStep.setDescription("Choix du signataire"); workflowStep.setChangeable(true); l'utilisateur n + 1 de l'utilisateur courant par ex if(data != null) { //User userManager = calculManager(userEppn) workflowStep.setRecipients(workflowService.getFavoriteRecipientEmail(1, data.getForm(), recipentEmailsStep, user)//workflowStep2.getUsers().add(userManager); workflowStep2.setChangeable(false); } else { workflowSteps.add(workflowStep2); workflowStep.getRecipients().add(recipientService.createRecipient(null, userService.getGenericUser("Utilisateur issue des favoris", ""))); } workflowSteps.add(workflowStep); return workflowSteps; } } return workflowSteps; } } |
Il faut donc a minima implémenter:
- getName() : en inscrivant un nom unique qui est le même que le nom de la classe
- getDescription() : qui sera visible au niveau de l'interface graphique.
- generateWorkflowSteps() : qui
- Préciser un nom et une description (dans name et description)
- Implémenter la fonction generateWorkflowSteps() qui retournera une liste de WorkFlowStep (les étapes calculée calculées en fonction de l'utilisateur courant "user")userEppn"). Le deuxième paramètre "workflowStepDto" représente les éléments transmis par l'utilisateur au moment ou il démarre le workflow
| Avertissement |
|---|
Attention cette classe est un @Component donc c'est un singleton. Tout attribut déclaré en dehors de la fonction generateWorkflowSteps sera commun à tous les utilisateurs ! |
Vous pourrez, par exemple, utiliser RestTemplate restTemplate = new RestTemplate(); pour lancer des requetes API (ajouter "import org.springframework.web.client.RestTemplate;").
Votre configuration LDAP sera accessible en ajoutant
@Ressource
private final LdapTemplate ldapTemplate;
aux attributs de la classe et ensuite lancer une requete : List<PersonLightLdap> personLightLdaps = ldapTemplate.search("eduPersonPrincipalName=" + userEppn, new PersonLightLdapAttributesMapper())
WorflowStep est defini comme suit : https://github.com/EsupPortail/esup-signature/blob/master/src/main/java/org/esupportail/esupsignature/entity/WorkflowStep.java
WorkflowStepDto : https://github.com/EsupPortail/esup-signature/blob/master/src/main/java/org/esupportail/esupsignature/dto/json/WorkflowStepDto.java
PersonLightLdap : https://github.com/EsupPortail/esup-signature/blob/master/src/main/java/org/esupportail/esupsignature/service/ldap/entry/PersonLightLdap.java
...
Récupérer les données via Power Query
...