Bug #13043
iparapheur : AttributeError: 'NoneType' object has no attribute 'MessageRetour'
100%
Description
Trace aujourd'hui :
Error occurred while processing request Traceback (most recent call last): File "/usr/lib/python2.7/dist-packages/passerelle/utils/jsonresponse.py", line 350, in api resp = f(*args, **kwargs) File "/usr/lib/python2.7/dist-packages/passerelle/contrib/iparapheur/views.py", line 53, in get return self.get_data(request, *args, **kwargs) File "/usr/lib/python2.7/dist-packages/passerelle/contrib/iparapheur/views.py", line 115, in get_data return self.get_object().get_file_status(kwargs['file_id']) File "/usr/lib/python2.7/dist-packages/passerelle/contrib/iparapheur/models.py", line 121, in get_file_status if resp.MessageRetour.codeRetour == 'KO': AttributeError: 'NoneType' object has no attribute 'MessageRetour'
Fichiers
Révisions associées
iparapheur: add handling of invalid response content (#13043)
Historique
Mis à jour par Josué Kouka il y a plus de 7 ans
- Assigné à mis à Josué Kouka
J'ai du mal à reproduire l'erreur. La trace complète est à cette adresse https://sentry.entrouvert.org/sentry/production/issues/827/.
Pour la meme requête le status m'est correctement renvoyé.
{ data: { status: "EnCoursVisa", nom: "Test Entrouvert", annotation: "Dossier déposé sur le bureau Direction Petite-Enfance-Courriers pour Visa", timestamp: "2016-07-29T15:03:06.000" }, err: 0 }
Mis à jour par Frédéric Péters il y a plus de 7 ans
C'est peut-être intéressant de lire le module SOAP utilisé et voir dans quelles circonstances il pourrait retourner None lors d'un appel de méthode. Ou bien juste accepter que c'est possible et traiter la situation.
Mis à jour par Benjamin Dauvergne il y a plus de 7 ans
On utilise suds avec comme transport requests et même le requests modifié pour logger il me semble, mais j'ai voulu aller regarder dans ufo les logs et malheureusement les logs ne passent plus :/
Mis à jour par Josué Kouka il y a plus de 7 ans
Frédéric Péters a écrit :
C'est peut-être intéressant de lire le module SOAP utilisé et voir dans quelles circonstances il pourrait retourner None lors d'un appel de méthode. Ou bien juste accepter que c'est possible et traiter la situation.
Le modules soap.py
du connecteur surcharge la seul method (send) suceptible de renvoyer un None
(https://fedorahosted.org/suds/browser/trunk/suds/transport/http.py#L83).
Par contre, j'ai pu reproduire l'erreur en passant une chaine vide a l'objet Reply
return Reply(resp.status_code, resp.headers, '')
Seul les logs (qu'on a plus sauf dans les archives de syslog) peuvent affirmer que la réponse de requests etait vide ...
Mis à jour par Josué Kouka il y a plus de 7 ans
J'ai pu retrouvé la trace dans la log
Sep 5 04:00:53 passerelle passerelle INFO passerelle.alfortville.fr 5.135.221.5 - r:48E0F10 GET https://iparapheur.mairie-alfortville.fr:4443/ws-iparapheur?wsdl 33 Sep 5 04:00:54 passerelle passerelle DEBUG passerelle.alfortville.fr 5.135.221.5 - r:48E0F10 Request Headers: Authorization: Basic ZW50cm91dmVydDpjaGF0ZWF1 | Accept-Encoding: gzip, deflate | Accept: */* | User-Agent: python-requests/2.3.0 CPython/2.7.3 Linux/2.6.32-3 7-pve | 34 Sep 5 04:00:54 passerelle passerelle INFO passerelle.alfortville.fr 5.135.221.5 - r:48E0F10 Status code: 200 35 Sep 5 04:00:54 passerelle passerelle DEBUG passerelle.alfortville.fr 5.135.221.5 - r:48E0F10 Response Headers: 'content-length: 56721 | accept-ranges: bytes | server: nginx/1.6.2 | last-modified: Thu, 14 Jan 2016 13:55:06 GMT | connection: keep-alive | etag: "5697a8b a-dd91" | cache-control: public | date: Mon, 05 Sep 2016 04:00:53 GMT | content-type: application/octet-stream | ' 36 Sep 5 04:00:54 passerelle passerelle DEBUG passerelle.alfortville.fr 5.135.221.5 - r:48E0F10 Response Content: '<?xml version="1.0" encoding="UTF-8"?>\n<wsdl:definitions xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/" xmlns:sch="http://www.adullact.org/spring-ws/iparap heur/1.0" \n\txmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/" xmlns:tns="http://www.adullact.org/spring-ws/iparapheur/1.0" \n\ttargetNamespace="http://www.adullact.org/spring-ws/iparapheur/1.0">\n <wsdl:types>\n <xsd:schema xmlns:xsd="http://www.w3.org/2001/XML Schema" xmlns:iph="http://www.adullact.org/spring-ws/iparapheur/1.0" xmlns:xmime="http://www.w3.org/2005/05/xmlmime" attributeFormDefault="qualified" elementFormDefault="qualified" targetNamespace="http://www.adullact.org/spring-ws/iparapheur/1.0">\n\n <xsd:import namespace="http://www.w3.org/2005/05/xmlmime" schemaLocation="http://www.w3.org/2005/05/xmlmime"/>\n\n\n <xsd:annotation>\n \t<xsd:documentation>\n \t\tWeb-Services pour i-Parapheur, version 3.4 (jan. 2013).\n \t</xsd:documentation>\n </xsd:annotation>\ n\n\t<!-- -->\n\t<!-- Web Service : Interrogation des Types disponibles -->\n\t<!-- -->\n\n <xsd:element name="GetListeTypesRequest"/>\n\n <xsd:element name="GetListeTypesResponse">\n\t<xsd:annotation>\n\t <xsd:documentation>Interrogation des Types disponible s</xsd:documentation>\n </xsd:annotation>\n <xsd:complexType>\n <xsd:sequence>\n <xsd:element maxOccurs="unbounded" minOccurs="0" name="TypeTechnique" type="iph:TypeTechnique"/>\n </xsd:sequence>\n </xsd:comple xType>\n </xsd:element>\n\n\t<!-- -->\n\t<!-- Web Service : Interrogation des Sous-Types disponibles -->\n\t<!-- -->\n\n <xsd:element name="GetListeSousTypesRequest" type="iph:TypeTechnique"/>\n\n <xsd:element name="GetListeSousTypesResponse">\n\t<xsd:annotat ion>\n\t <xsd:documentation>Interrogation des Sous-Types disponibles</xsd:documentation>\n </xsd:annotation>\n <xsd:complexType>\n <xsd:sequence>\n <xsd 37 Sep 5 04:00:54 passerelle passerelle INFO passerelle.alfortville.fr 5.135.221.5 - r:48E0F10 POST https://iparapheur.mairie-alfortville.fr:4443/ws-iparapheur 38 Sep 5 04:00:54 passerelle passerelle DEBUG passerelle.alfortville.fr 5.135.221.5 - r:48E0F10 Request Headers: Content-Length: 482 | Accept-Encoding: gzip, deflate | SOAPAction: "" | Accept: */* | User-Agent: python-requests/2.3.0 CPython/2.7.3 Linux/2.6.32-37-pve | C ontent-Type: text/xml; charset=utf-8 | Authorization: Basic ZW50cm91dmVydDpjaGF0ZWF1 | 39 Sep 5 04:00:54 passerelle passerelle INFO passerelle.alfortville.fr 5.135.221.5 - r:48E0F10 Request Payload: '<?xml version="1.0" encoding="UTF-8"?><SOAP-ENV:Envelope xmlns:ns0="http://schemas.xmlsoap.org/soap/envelope/" xmlns:ns1="http://www.adullact.org/spring-ws/i parapheur/1.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xm="http://www.w3.org/2005/05/xmlmime"><SOAP-ENV:Header/><ns0:Body><ns1:GetHistoDossierRequest>7d308fb8-2b33-46c5-ae61-6d20f115d95d</n s1:GetHistoDossierRequest></ns0:Body></SOAP-ENV:Envelope>' 40 Sep 5 04:00:54 passerelle passerelle INFO passerelle.alfortville.fr 5.135.221.5 - r:48E0F10 Status code: 502 41 Sep 5 04:00:54 passerelle passerelle DEBUG passerelle.alfortville.fr 5.135.221.5 - r:48E0F10 Response Headers: 'date: Mon, 05 Sep 2016 04:00:54 GMT | content-length: 172 | content-type: text/html | connection: keep-alive | server: nginx/1.6.2 | ' 42 Sep 5 04:00:54 passerelle passerelle DEBUG passerelle.alfortville.fr 5.135.221.5 - r:48E0F10 Response Content: '<html>\r\n<head><title>502 Bad Gateway</title></head>\r\n<body bgcolor="white">\r\n<center><h1>502 Bad Gateway</h1></center>\r\n<hr><center>nginx/1.6.2</ce nter>\r\n</body>\r\n</html>\r\n' 43 Sep 5 04:00:54 passerelle passerelle ERROR passerelle.alfortville.fr 5.135.221.5 - r:48E0F10 Error occurred while processing request#012Traceback (most recent call last):#012 File "/usr/lib/python2.7/dist-packages/passerelle/utils/jsonresponse.py", line 350, in api# 012 resp = f(*args, **kwargs)#012 File "/usr/lib/python2.7/dist-packages/passerelle/contrib/iparapheur/views.py", line 53, in get#012 return self.get_data(request, *args, **kwargs)#012 File "/usr/lib/python2.7/dist-packages/passerelle/contrib/iparapheur/views. py", line 115, in get_data#012 return self.get_object().get_file_status(kwargs['file_id'])#012 File "/usr/lib/python2.7/dist-packages/passerelle/contrib/iparapheur/models.py", line 121, in get_file_status#012 if resp.MessageRetour.codeRetour == 'KO':#012Attribu teError: 'NoneType' object has no attribute 'MessageRetour'
Une 502, du coup le content etait du html
Mis à jour par Benjamin Dauvergne il y a plus de 7 ans
Donc on reçoit un 502 et ça passe comme une lettre à la poste ? Il y a peut-être un souci dans l'implémentation du transport suds, non ?
Mis à jour par Serghei Mihai il y a plus de 7 ans
Oui, il manque un raise_for_status
dans la méthode send
du transport.
Mis à jour par Benjamin Dauvergne il y a plus de 7 ans
suds prévoit une exception particulière pour ces cas, TransportError
; il faudrait s'en servir et ne pas juste faire raise_for_status()
, voir le code dans suds/transport/https.py:
def send(self, request): result = None url = request.url msg = request.message headers = request.headers try: u2request = u2.Request(url, msg, headers) self.addcookies(u2request) self.proxy = self.options.proxy request.headers.update(u2request.headers) log.debug('sending:\n%s', request) fp = self.u2open(u2request) self.getcookies(fp, u2request) result = Reply(200, fp.headers.dict, fp.read()) log.debug('received:\n%s', result) except u2.HTTPError, e: if e.code in (202,204): result = None else: raise TransportError(e.msg, e.code, e.fp) return result
Mis à jour par Josué Kouka il y a plus de 7 ans
- Fichier 0001-iparapheur-add-handling-of-invalid-response-content.patch 0001-iparapheur-add-handling-of-invalid-response-content.patch ajouté
Benjamin Dauvergne a écrit :
suds prévoit une exception particulière pour ces cas,
TransportError
; il faudrait s'en servir et ne pas juste faireraise_for_status()
, voir le code dans suds/transport/https.py:[...]
J'ai fait un patch ou je m'amuse un peu avec le TransportError
et l'utiliser ne me plait personnelement pas.
J'ai l'impréssion que Suds fait des trucs bizarres dans le traitement de cette Exception.
L'exception reçue par le jsonresponse
n'a aucun des attributs définis au depart (y compris ceux ajoutés par mon CustomTransportError ), du coup elle ne peut etre traité comme les autres exceptions d'autres connecteurs (status_code always 500).
De plus l'exception n'est pas très descriptive :
{"err_class": "xml.sax._exceptions.SAXParseException", "err_desc": "<unknown>:1:0: syntax error", "data": null, "err": 1}
Mis à jour par Josué Kouka il y a plus de 7 ans
Josué Kouka a écrit :
Benjamin Dauvergne a écrit :
suds prévoit une exception particulière pour ces cas,
TransportError
; il faudrait s'en servir et ne pas juste faireraise_for_status()
, voir le code dans suds/transport/https.py:[...]
J'ai fait un patch ou je m'amuse un peu avec le
TransportError
et l'utiliser ne me plait personnelement pas.
J'ai l'impréssion que Suds fait des trucs bizarres dans le traitement de cette Exception.
L'exception reçue par lejsonresponse
n'a aucun des attributs définis au depart (y compris ceux ajoutés par mon CustomTransportError ), du coup elle ne peut etre traité comme les autres exceptions d'autres connecteurs (status_code always 500).
De plus l'exception n'est pas très descriptive :
[...]
Une aurtre solution serait de faire des try/except
partout ou l'objet client
est appelé pour capture l'exception XML et raiser une CustomException
avec le bon http_status et la bonne description (Invalid Response Content, XML expected)
Mis à jour par Benjamin Dauvergne il y a plus de 7 ans
Il faut faire les deux. Il y a des couches d'abstractions à différents niveaux, on ne doit pas les casser donc suds doit renvoyer des TransportError pas des HttpError et le code qui appelle suds doit intercepter les erreurs suds pour en faire quelque chose que json_api comprenne.
Mis à jour par Josué Kouka il y a plus de 7 ans
- Fichier 0003-iparapheur-misc-remove-useless-import-and-variables.patch 0003-iparapheur-misc-remove-useless-import-and-variables.patch ajouté
- Fichier 0002-iparapheur-add-permission-to-endpoints.patch 0002-iparapheur-add-permission-to-endpoints.patch ajouté
- Fichier 0001-iparapheur-add-handling-of-invalid-response-content.patch 0001-iparapheur-add-handling-of-invalid-response-content.patch ajouté
Mis à jour par Josué Kouka il y a plus de 7 ans
- Fichier 0003-iparapheur-misc-remove-useless-import-and-variables.patch 0003-iparapheur-misc-remove-useless-import-and-variables.patch ajouté
- Fichier 0002-iparapheur-add-permission-to-endpoints.patch 0002-iparapheur-add-permission-to-endpoints.patch ajouté
- Fichier 0001-iparapheur-add-handling-of-invalid-response-content.patch 0001-iparapheur-add-handling-of-invalid-response-content.patch ajouté
- Patch proposed changé de Non à Oui
Mis à jour par Benjamin Dauvergne il y a plus de 7 ans
except(Exception,) as e:
Sauvage ! Tu te fais chier à raiser de beaux TransportError et puis tu catches tout... donc tu catches juste TransportError, et vu que c'est plein de détail dedans tu les mets dans ton InvalidResponseContent en plus t'as peut-être une 500 avec du XML dedans tu n'en sais rien (je vais proposer deux simples APIError et APIWarning bientôt pour qu'on arrête de s'emmerder à créer des classes d'exception). Donc je mettrai comme message:
'Transport Error : '%(code erreur transport)', received content: %(extrait du contenu limité à 1000 caractères)'.
Il faudrait voir si suds ne renvoie pas d'autres erreurs intéressantes par exemple si le document XML est invalide ou en cas de SOAP:Error.
Les deux autres patchs n'ont pas l'air d'avoir de rapport avec ce ticket, si ?
Mis à jour par Josué Kouka il y a plus de 7 ans
Benjamin Dauvergne a écrit :
[...]
Sauvage ! Tu te fais chier à raiser de beaux TransportError et puis tu catches tout... donc tu catches juste TransportError, et vu que c'est plein de détail dedans tu les mets dans ton InvalidResponseContent en plus t'as peut-être une 500 avec du XML dedans tu n'en sais rien (je vais proposer deux simples APIError et APIWarning bientôt pour qu'on arrête de s'emmerder à créer des classes d'exception). Donc je mettrai comme message: [...]
J'etais obligé de TOUT catcher. Suds passe le TransportError
à l'object Client
qui lui en cas d'érreur renvoit juste une Exception
.
Il faudrait voir si suds ne renvoie pas d'autres erreurs intéressantes par exemple si le document XML est invalide ou en cas de SOAP:Error.
Justement, il renvoit un jolie Exception
(https://fedorahosted.org/suds/browser/trunk/suds/client.py#L718)
Les deux autres patchs n'ont pas l'air d'avoir de rapport avec ce ticket, si ?
Eux non, c'est des miscs et je ne pensais pas necessaire de faire des tickets pour (sauf si....)
Mis à jour par Benjamin Dauvergne il y a plus de 7 ans
Josué Kouka a écrit :
Benjamin Dauvergne a écrit :
[...]
Sauvage ! Tu te fais chier à raiser de beaux TransportError et puis tu catches tout... donc tu catches juste TransportError, et vu que c'est plein de détail dedans tu les mets dans ton InvalidResponseContent en plus t'as peut-être une 500 avec du XML dedans tu n'en sais rien (je vais proposer deux simples APIError et APIWarning bientôt pour qu'on arrête de s'emmerder à créer des classes d'exception). Donc je mettrai comme message: [...]
J'etais obligé de TOUT catcher. Suds passe le
TransportError
à l'objectClient
qui lui en cas d'érreur renvoit juste uneException
.
Effectivement c'est très moche néanmoins il faut avancer, donc on va réécrire Client.failed() et ça va renvoyer la TransportError.
Ensuite j'ai extrait la liste des exceptions que suds peut envoyer en dehors de ce TransportError qui devient un Exception:
./resolver.py: class BadPath(Exception): pass ./transport/__init__.py:class TransportError(Exception): ./__init__.py:class MethodNotFound(Exception): ./__init__.py:class PortNotFound(Exception): ./__init__.py:class ServiceNotFound(Exception): ./__init__.py:class TypeNotFound(Exception): ./__init__.py:class BuildError(Exception): ./__init__.py:class SoapHeadersNotPermitted(Exception): ./__init__.py:class WebFault(Exception):
Je crois qu'il n'y a que MethodNotFound et WebFault qu'on peut avoir lors d'un appel mais c'est à vérifier.
Il faudrait voir si suds ne renvoie pas d'autres erreurs intéressantes par exemple si le document XML est invalide ou en cas de SOAP:Error.
Justement, il renvoie un jolie
Exception
(https://fedorahosted.org/suds/browser/trunk/suds/client.py#L718)Les deux autres patchs n'ont pas l'air d'avoir de rapport avec ce ticket, si ?
Eux non, c'est des miscs et je ne pensais pas necessaire de faire des tickets pour (sauf si....)
Si juste pour pas mélanger les relectures, autant le troisième patch ne fait rien mais le deuxième si.
Mis à jour par Josué Kouka il y a plus de 7 ans
Benjamin Dauvergne a écrit :
La méthode est malheureusement attchée àJosué Kouka a écrit :
Benjamin Dauvergne a écrit :
[...]
Sauvage ! Tu te fais chier à raiser de beaux TransportError et puis tu catches tout... donc tu catches juste TransportError, et vu que c'est plein de détail dedans tu les mets dans ton InvalidResponseContent en plus t'as peut-être une 500 avec du XML dedans tu n'en sais rien (je vais proposer deux simples APIError et APIWarning bientôt pour qu'on arrête de s'emmerder à créer des classes d'exception). Donc je mettrai comme message: [...]
J'etais obligé de TOUT catcher. Suds passe le
TransportError
à l'objectClient
qui lui en cas d'érreur renvoit juste uneException
.Effectivement c'est très moche néanmoins il faut avancer, donc on va réécrire Client.failed() et ça va renvoyer la TransportError.
SoapClient
qui lui prend l'objet Client
comme attribut.
- https://fedorahosted.org/suds/browser/trunk/suds/client.py#L534
- https://fedorahosted.org/suds/browser/trunk/suds/client.py#L576
Dans cette situation ça dérange tant que ça que l'on traite les réponses vides de manière personnalisée (éviter des les gérer via Suds)
Ensuite j'ai extrait la liste des exceptions que suds peut envoyer en dehors de ce TransportError qui devient un Exception:
[...]
Je crois qu'il n'y a que MethodNotFound et WebFault qu'on peut avoir lors d'un appel mais c'est à vérifier.
Il faudrait voir si suds ne renvoie pas d'autres erreurs intéressantes par exemple si le document XML est invalide ou en cas de SOAP:Error.
Justement, il renvoie un jolie
Exception
(https://fedorahosted.org/suds/browser/trunk/suds/client.py#L718)Les deux autres patchs n'ont pas l'air d'avoir de rapport avec ce ticket, si ?
Eux non, c'est des miscs et je ne pensais pas necessaire de faire des tickets pour (sauf si....)
Si juste pour pas mélanger les relectures, autant le troisième patch ne fait rien mais le deuxième si.
Mis à jour par Benjamin Dauvergne il y a plus de 7 ans
Ouais ok c'est vraiment moche, donc on soit WebFault soit Exception((status, reason)), je dirai de faire un if not isinstane(e.args[0], tuple): raise
pour tenter de n'intercepter que les TransportError
convertis.
try: ... except WebFault, e: # voir ce qu'on peut faire avec ça except Exception, e: if not(e.args) or not isintance(e.args[0], tuple): raise # c'est une TransportError... raise ProxyError(e.args[0][1], err_code=e.args[0][0]) # un truc dans le genre
Mis à jour par Josué Kouka il y a plus de 7 ans
Mis à jour par Benjamin Dauvergne il y a plus de 7 ans
- typo:
FiteNotFoundError
- Il faut catcher les RequestError autour de
resp = self.model.requests.post(request.url, data=request.message, headers=request.headers, **self.get_requests_kwargs())
, et renvoyerTransportError(e.args[0], None)
. Car il n'y a pas que les codes d'erreur HTTP qui nous intéresse, il y a aussi les erreurs de connexion ou de résolution de nom. - Il faut mettre un commentaire pour expliquer ce qui se passe là:
if not(e.args) or not isinstance(e.args[0], tuple): raise e
InvalidResponseContent('Invalid Response Content, XML expected')
Ce message est faux, on ne sait pas si c'est un problème de XML ou autre il faut récupérer e.args0 pour avoir une idée du problème.
Mis à jour par Josué Kouka il y a plus de 7 ans
Mis à jour par Serghei Mihai il y a plus de 7 ans
from requests.execptions import RequestException
ne va pas fonctionner.
Mis à jour par Frédéric Péters il y a plus de 7 ans
Mais comment on se trouve avec un test mais pas de détection de cette erreur ?
Mis à jour par Josué Kouka il y a plus de 6 ans
- Fichier 0002-iparapheur-add-handling-of-invalid-response-content.patch 0002-iparapheur-add-handling-of-invalid-response-content.patch ajouté
- Fichier 0001-iparapheur-misc-remove-useless-import-and-variables.patch 0001-iparapheur-misc-remove-useless-import-and-variables.patch ajouté
- Statut changé de Nouveau à En cours
Le patch avec les corrections.
Vous remarquerez que dans create_file
je n'utilise pas self.call
à cause du c.factory.create('TypeDoc')
appelé en amont.
Vu que ce n'est pas l'object du ticket, j'ai laissé comme tel et créé #17714 qui de devrait pallier ce problème.
Mis à jour par Josué Kouka il y a plus de 6 ans
- Fichier 0002-iparapheur-add-handling-of-invalid-response-content.patch 0002-iparapheur-add-handling-of-invalid-response-content.patch ajouté
- Fichier 0001-iparapheur-misc-remove-useless-import-and-variables.patch 0001-iparapheur-misc-remove-useless-import-and-variables.patch ajouté
Suppression d'exceptions inutile
Mis à jour par Josué Kouka il y a plus de 6 ans
- Fichier 0002-iparapheur-add-handling-of-invalid-response-content.patch 0002-iparapheur-add-handling-of-invalid-response-content.patch ajouté
- Fichier 0001-iparapheur-misc-remove-useless-import-and-variables.patch 0001-iparapheur-misc-remove-useless-import-and-variables.patch ajouté
Avec rebase sur #17068
Mis à jour par Josué Kouka il y a plus de 6 ans
- Fichier 0002-iparapheur-add-handling-of-invalid-response-content.patch 0002-iparapheur-add-handling-of-invalid-response-content.patch ajouté
- Fichier 0001-iparapheur-misc-remove-useless-import-and-variables.patch 0001-iparapheur-misc-remove-useless-import-and-variables.patch ajouté
Suppression de saut de lignes ajoutés inutiles
Mis à jour par Josué Kouka il y a plus de 6 ans
- Statut changé de En cours à Résolu (à déployer)
- % réalisé changé de 0 à 100
Mis à jour par Benjamin Dauvergne il y a plus de 5 ans
- Statut changé de Résolu (à déployer) à Fermé
iparapheur: misc, remove useless import and variables (#13043)