Kaynağa Gözat

amelioration schemas

MASSOT Olivier 7 yıl önce
ebeveyn
işleme
0169351233

BIN
GDAL-2.3.2-cp37-cp37m-win32.whl


+ 29 - 9
core/cerberus_extend.py

@@ -7,7 +7,6 @@ import locale
 import re
 
 import cerberus
-import chardet
 
 from core import gis
 
@@ -16,7 +15,16 @@ def is_french_date(field, value, error):
     try:
         datetime.strptime(value, '%d/%m/%Y')
     except:
-        error(field, 'Doit être une date au format jj/mm/aaaa')
+        error(field, 'Doit être une date au format jj/mm/aaaa: {}'.format(value))
+
+def is_modern_french_date(field, value, error):
+    try:
+        d = datetime.strptime(value, '%d/%m/%Y')
+        if not d.year >= 2000:
+            error(field, "La date ne peut pas être antérieure à l'an 2000: {}".format(value))
+    except:
+        error(field, 'Doit être une date au format jj/mm/aaaa: {}'.format(value))
+
 
 def is_int(field, value, error):
     try:
@@ -26,24 +34,34 @@ def is_int(field, value, error):
         error(field, 'Doit être un nombre entier: {}'.format(value))
 
 def is_float(field, value, error):
-    value = locale.atof(str(value))
     try:
+        value = locale.atof(str(value))
         if  value != float(value):
             error(field, 'Doit être un nombre décimal ({})'.format(value))
     except ValueError:
         error(field, 'Doit être un nombre décimal ({})'.format(value))
 
+
 # Ref: http://docs.python-cerberus.org/en/stable/api.html#error-codes
 
-class GeoValidator(cerberus.validator.Validator):
-    
+class ExtendedValidator(cerberus.validator.Validator):
+
     def __init__(self, *args, **kwargs):
-        super(GeoValidator, self).__init__(*args, **kwargs)
+        super(ExtendedValidator, self).__init__(*args, **kwargs)
         
         # Rends tous les champs requis par défaut, à moins que 'required' ait été défini dans le schéma
         for field in self.schema:
             if not 'required' in self.schema[field]:
                 self.schema[field]['required'] = True
+
+    def _validate_contains_any_of(self, iterable, field, value):
+        """ Controle que la chaine contient l'une des substring contenue dans l'iterable
+
+        The rule's arguments are validated against this schema:
+        {'type': 'list'}
+        """
+        if not re.search(r".*{}.*".format("|".join(iterable)), value):
+            self._error(field, "Doit contenir un de ces éléments: {} ('{}')".format(", ".join(iterable), value))
         
     def _validate_multiallowed(self, allowed, field, value):
         """ Comme 'allowed', mais autorise plusieurs valeurs séparées par un '-'
@@ -54,7 +72,9 @@ class GeoValidator(cerberus.validator.Validator):
         for item in re.split("\s?-\s?", value):
             if not item in allowed:
                 self._error(field, "Valeur non-autorisée: {}".format(item))
-        
+                
+
+class GeoValidator(ExtendedValidator):
     
     def _validate_geometry(self, geom, field, value):
         """ Contrôle la géométrie d'un objet
@@ -102,8 +122,8 @@ class CerberusErrorHandler(cerberus.errors.BasicErrorHandler):
                 0x24: "Doit être du type {constraint}: {value}",
                 0x25: "Doit être de type 'dictionnaire': {value}",
                 0x26: "La longueur de la liste doit être de {constraint}, elle est de {0}",
-                0x27: "La longueur minimum du champs est de {constraint}",
-                0x28: "Trop long, la longueur max. du champs est de {constraint}",
+                0x27: "La longueur minimum du champs est de {constraint}: {value}",
+                0x28: "Trop long, la longueur max. du champs est de {constraint}: {value}",
 
                 0x41: "La valeur n'est pas au bon format ('{constraint}')",
                 0x42: "La valeur minimum autorisée est {constraint}",

+ 10 - 0
core/gis.py

@@ -24,6 +24,16 @@ SHAPE_NAMES = {0: "(AUCUN)",
                }
 
 
+def same_geom(geom1, geom2, buffer = 0):
+    """ retourne vrai si les deux géométries sont identiques """
+    if geom1.shapeType == geom2.shapeType:
+        if geom1.shapeType in (3,5,8,13,15,18,23,25,28,31):
+            return geom1.bbox == geom2.bbox
+        else:
+            return geom1 == geom2
+        
+    return False
+
 class ShapeError(IOError): 
     pass
 class SridError(IOError): 

+ 2 - 2
core/mn.py

@@ -4,11 +4,11 @@
 '''
 from core.db import PostgresDb
 
-class ReferentielDb(PostgresDb):
+class ANTDb(PostgresDb):
     server = "clusterpg.linux.infra.cloud.local"
     port="5432"
     db = "sig50"
     user = "sigr"
     pwd = "T38Msh2R4q"
     def __init__(self, **kwargs):
-        super(ReferentielDb, self).__init__(server=self.server, dbname=self.db, user=self.user, pwd=self.pwd, **kwargs)
+        super(ANTDb, self).__init__(server=self.server, dbname=self.db, user=self.user, pwd=self.pwd, **kwargs)

+ 24 - 9
core/validation.py

@@ -71,6 +71,7 @@ class WrongSrid(BaseValidationError):
 class DataError(BaseValidationError):
     name = "Erreur de format"
     level = ERROR
+#     level = CRITICAL
 
 # Erreurs dans le contenu, erreurs métiers
 
@@ -81,9 +82,17 @@ class DuplicatedPk(TechnicalValidationError):
     name = "Doublons dans le champs"
 
 class RelationError(TechnicalValidationError):
+    level = CRITICAL
     name = "Un objet lié n'existe pas"
 
+class DuplicatedGeom(TechnicalValidationError):
+    name = "Doublon graphique"
+
+class MissingItem(TechnicalValidationError):
+    name = "Elément manquant"
 
+class DimensionError(TechnicalValidationError):
+    name = "Elément manquant"
 
 class BaseValidator():
     schema_name = ""
@@ -96,12 +105,16 @@ class BaseValidator():
         self.errors = []
         self.dt = 0
     
-    def checkpoint(self, name):
-        valid = (len(self.errors) == 0)
-        self.checkpoints.append(Checkpoint(name, valid))
-        if not valid:
+    def checkpoint(self, title):
+        
+        self.checkpoints.append(Checkpoint(title, (not self.errors)))
+        if self.errors:
             self.valid = False
-            raise ValidatorInterruption()
+            if self.critical_happened():
+                raise ValidatorInterruption()
+             
+    def critical_happened(self):
+        return any([err.level == CRITICAL for err in self.errors])
              
     def log_error(self, validation_error):
         self.errors.append(validation_error)
@@ -150,9 +163,11 @@ class BaseValidator():
         self.checkpoint("Contrôle de la structure des données")
         
         # Validation technique
-        self._technical_validation()
-        self.checkpoint("Validation Métier")
-    
+        try:
+            self._technical_validation()
+            self.checkpoint("Validation Métier")
+        except:
+            self.checkpoint("Validation Métier [interrompu]")
     
     def _load_files(self, folder):
         """ Charge les données du fichier et les associe à un modèle.
@@ -162,7 +177,7 @@ class BaseValidator():
     def _structure_validation(self):
         
         for model in self.models:
-            v = GeoValidator(model.schema, purge_unknown=True, error_handler=CerberusErrorHandler)
+            v = GeoValidator(model.schema, purge_unknown=True, error_handler=CerberusErrorHandler, require_all=True)
             
             for item in self.dataset[model]:
 

+ 64 - 0
readme.md

@@ -0,0 +1,64 @@
+# Datachecker
+
+Outil de contrôle livrables FTTH.
+
+Les formats actuellement acceptés sont
+
+* Format MN 1.12
+* Format Netgeo 2.2
+
+
+## Démarrage
+
+Ouvrir une fenêtre de commande dans le répertoire de l'application, et lancer:
+
+	set FLASK_APP=index.py
+	flask run
+	
+Le serveur est alors disponible à l'adresse suivante: [http://127.0.0.1:5000](http://127.0.0.1:5000)
+
+## Fonctionnement général
+
+Les tests se font en trois temps:
+
+1. Chargement des fichiers: on contrôle la présence des fichiers attendus, leur format, leur type de géométrie.
+2. Contrôle de la structure: on vérifie la structure des données, comme la présence des champs obligatoires, le type de données...etc.
+3. Validation métier: contrôles complémentaires sur la cohérence des données.
+
+> Une erreur critique au cours d'une de ces étapes interrompt les tests.
+
+## Schémas
+
+Les fichiers de configuration des schémas sont dans le sous-dossier `schemas`
+
+Le fichier `models.py` contient la définition des objets attendus, dont le nom du fichier contenant les données, et la structure des données attendues.
+Le fichier `validator.py` contient les fonctions de contrôle métier, et d'éventuelles modifications au chargement des données et au contrôle de la structure.
+
+## Tests effectués
+
+> Une liste plus détaillée est donnée dans le readme de chaque schéma.
+
+* **Chargement des données**
+
+  * Présence des fichiers attendus
+  * Format des fichiers
+  * Type de géométrie
+  * Projection (à venir)
+
+* **Structure**
+
+  * Les coordonnées des entités sont dans la zone d'emprise autorisée
+  * Les champs attendus sont présents
+  * Les champs obligatoires sont renseignés
+  * Type des données (entiers, décimaux, dates...)
+  * Les champs ne dépassent pas la longueur autorisée
+  * Contrôle des valeurs autorisées
+  
+* **Validation Métier**
+
+  * Contrôle des doublons dans les PK
+  * Contrôle des cardinalités
+  * Contrôle des doublons graphiques
+  * Autres contrôles liés au schéma.
+
+

+ 1 - 0
requirements.txt

@@ -5,3 +5,4 @@ pyshp
 Jinja2
 Flask
 cerberus
+.\GDAL-2.3.2-cp37-cp37m-win32.whl

BIN
resources/Modele_donnees_V1.12.xlsx


BIN
resources/_CONTROLE_DONNEES.xlsx


+ 4 - 2
schemas/common.py

@@ -8,5 +8,7 @@ from core import mn
 XMIN, XMAX, YMIN, YMAX = 1341999, 1429750, 8147750, 8294000
 SRID = 3949
 
-with mn.ReferentielDb() as ref_db:
-    INSEE_VALIDES = [row[0] for row in ref_db.read("SELECT code_insee FROM sig_referentiel.admn_cd50_com;")]
+BUFFER = 2
+
+with mn.ANTDb() as ant_db:
+    INSEE_VALIDES = [row[0] for row in ant_db.read("SELECT code_insee FROM sig_referentiel.admn_cd50_com;")]

+ 33 - 14
schemas/netgeo_1_12_doe/models.py

@@ -3,7 +3,8 @@
 @author: olivier.massot, 2018
 '''
 from core import gis
-from core.cerberus_extend import is_french_date, is_int, is_float
+from core.cerberus_extend import is_int, is_float, \
+    is_modern_french_date
 from core.validation import BaseGeoModel
 from schemas.common import INSEE_VALIDES, XMIN, YMIN, XMAX, YMAX
 
@@ -15,9 +16,10 @@ class Artere(BaseGeoModel):
               'AR_LONG': {'empty': False, 'validator': is_float},
               'AR_ETAT': {'type': 'string', 'empty': False, 'allowed': ['0', '1', '2', '3', '4']}, 
               'AR_OCCP': {'type': 'string', 'empty': False, 'allowed': ['0', '1.1', '1.2', '2', '3', '4']}, 
-              'AR_NOEUD_A': {'type': 'string', 'maxlength': 20}, 
-              'AR_NOEUD_B': {'type': 'string', 'maxlength': 20}, 
-              'AR_NB_FOUR': {'type': 'string', 'maxlength': 10}, 
+              'AR_NOEUD_A': {'type': 'string', 'empty': False, 'maxlength': 20}, 
+              'AR_NOEUD_B': {'type': 'string', 'empty': False, 'maxlength': 20}, 
+              'AR_NB_FOUR': {'empty': False, 'validator': is_int}, 
+#               'AR_NB_FOUR': {'type': 'string', 'maxlength': 10}, 
               'AR_FOU_DIS': {'empty': False, 'validator': is_int}, 
               'AR_TYPE_FO': {'type': 'string', 'empty': False, 'multiallowed': ['PVC', 'PEHD', 'SOUS-TUBAGE PEHD', 'SOUS-TUBAGE  SOUPLE', 'FACADE', 'AERIEN', 'ENCORBELLEMENT', 'AUTRE']}, 
               'AR_DIAM_FO': {'type': 'string', 'empty': False, 'multiallowed': ['10', '14', '18', '25', '28', '32', '40', '45', '60', 'NUL']}, 
@@ -25,8 +27,8 @@ class Artere(BaseGeoModel):
               'AR_PRO_CAB': {'type': 'string', 'empty': False, 'allowed': ['MANCHE NUMERIQUE']}, 
               'AR_GEST_FO': {'type': 'string', 'empty': False, 'multiallowed': ['MANCHE NUMERIQUE', 'MANCHE TELECOM', 'COLLECTIVITE', 'ORANGE', 'MANCHE FIBRE', 'PRIVE', 'ERDF', 'AUTRE (à préciser)', 'NUL']}, 
               'AR_UTIL_FO': {'type': 'string', 'empty': False, 'multiallowed': ['MANCHE NUMERIQUE', 'MANCHE TELECOM', 'COLLECTIVITE', 'ORANGE', 'PRIVE', 'AUTRE (à préciser)', 'NUL']}, 
-              'AR_DATE_IN': {'empty': False, 'validator': is_french_date}, 
-              'AR_DATE_RE': {'empty': False, 'validator': is_french_date}, 
+              'AR_DATE_IN': {'empty': False, 'validator': is_modern_french_date}, 
+              'AR_DATE_RE': {'empty': False, 'validator': is_modern_french_date}, 
               'AR_REF_PLA': {'type': 'string', 'maxlength': 100}, 
               'AR_SRC_GEO': {'type': 'string', 'maxlength': 50}, 
               'AR_QLT_GEO': {'type': 'string', 'empty': False, 'allowed': ['A', 'B', 'C']}, 
@@ -34,6 +36,9 @@ class Artere(BaseGeoModel):
               'AR_COMMENT': {'type': 'string', 'maxlength': 300, 'empty': True}, 
               'AR_STATUT': {'type': 'string', 'empty': False, 'allowed': ['APS', 'APD', 'EXE', 'REC']}}
 
+    def __repr__(self):
+        return "Artere {}-{}".format(self.AR_NOEUD_A, self.AR_NOEUD_B)
+
 class Cable(BaseGeoModel):
     filename = "cable_geo.shp"
     schema = {'geom': {'geometry': (gis.POLYLINE, (XMIN, YMIN, XMAX, YMAX))}, 
@@ -51,14 +56,17 @@ class Cable(BaseGeoModel):
               'CA_NB_FO_D': {'empty': False, 'validator': is_int}, 
               'CA_PRO': {'type': 'string', 'maxlength': 20, 'empty': False, 'allowed': ['MANCHE NUMERIQUE']}, 
               'CA_GEST': {'type': 'string', 'maxlength': 20, 'empty': False, 'allowed': ['MANCHE FIBRE']}, 
-              'CA_DATE_IN': {'empty': False, 'validator': is_french_date}, 
+              'CA_DATE_IN': {'empty': False, 'validator': is_modern_french_date}, 
               'CA_COMMENT': {'type': 'string', 'maxlength': 300, 'empty': True}, 
               'CA_STATUT': {'type': 'string', 'maxlength': 14, 'empty': False, 'allowed': ['APS', 'APD', 'EXE', 'REC']}}
 
+    def __repr__(self):
+        return "Cable {}-{}".format(self.CA_EQ_A, self.CA_EQ_B)
+    
 class Equipement(BaseGeoModel):
     filename = "equipement_passif.shp"
     schema = {'geom': {'geometry': (gis.POINT, (XMIN, YMIN, XMAX, YMAX))}, 
-              'EQ_NOM': {'type': 'string', 'maxlength': 10}, 
+              'EQ_NOM': {'type': 'string', 'maxlength': 10, 'contains_any_of': ['PBO', 'BPE', 'BAI']}, 
               'EQ_NOM_NOE': {'type': 'string', 'maxlength': 14}, 
               'EQ_ETAT': {'type': 'string', 'maxlength': 1, 'empty': False, 'allowed': ['0', '1', '2', '3', '4']}, 
               'EQ_OCCP': {'type': 'string', 'maxlength': 3, 'empty': False, 'allowed': ['0', '1.1', '1.2', '2', '3', '4']}, 
@@ -68,9 +76,12 @@ class Equipement(BaseGeoModel):
               'EQ_PRO': {'type': 'string', 'maxlength': 20, 'empty': False, 'allowed': ['MANCHE NUMERIQUE', 'COLLECTIVITE', 'ORANGE', 'PRIVE', 'AUTRE (à préciser)', 'NUL']}, 
               'EQ_GEST': {'type': 'string', 'maxlength': 20, 'empty': False, 'allowed': ['MANCHE NUMERIQUE', 'MANCHE TELECOM', 'COLLECTIVITE', 'ORANGE', 'MANCHE FIBRE', 'PRIVE', 'AUTRE (à préciser)', 'NUL']}, 
               'EQ_HAUT': {'empty': False, 'validator': is_float}, 
-              'EQ_DATE_IN': {'empty': False, 'validator': is_french_date}, 
+              'EQ_DATE_IN': {'empty': False, 'validator': is_modern_french_date}, 
               'EQ_COMMENT': {'type': 'string', 'maxlength': 300, 'empty': True}, 
               'EQ_STATUT': {'type': 'string', 'maxlength': 14, 'empty': False, 'allowed': ['APS', 'APD', 'EXE', 'REC']}}
+        
+    def __repr__(self):
+        return "Equipement {}".format(self.EQ_NOM)
 
 class Noeud(BaseGeoModel):
     filename = "noeud_geo.shp"
@@ -84,7 +95,7 @@ class Noeud(BaseGeoModel):
               'NO_TYPE_LQ': {'type': 'string', 'maxlength': 10, 'empty': False, 'allowed': ['CHTIR', 'CHRACC', 'POT', 'NRO', 'PM', 'MIMO', 'FAC', 'OUV', 'IMM']}, 
               'NO_TYPE_PH': {'type': 'string', 'maxlength': 20, 'empty': False, 'allowed': ['CHAMBRE', 'POTEAU', 'ARMOIRE', 'SHELTER', 'BATIMENT', 'SITE MIMO', 'FACADE', 'OUVRAGE', 'IMMEUBLE']}, 
               'NO_CODE_PH': {'type': 'string', 'maxlength': 20}, 
-              'NO_TECH_PS': {'type': 'string', 'maxlength': 4, 'empty': False, 'multiallowed': ['COAX', 'CUT', 'ECL', 'ELEC', 'VP', 'OPT', 'NC']}, 
+              'NO_TECH_PS': {'type': 'string', 'maxlength': 20, 'empty': False, 'multiallowed': ['COAX', 'CUT', 'ECL', 'ELEC', 'VP', 'OPT', 'NC']}, 
               'NO_AMO': {'type': 'string', 'maxlength': 20}, 
               'NO_PLINOX': {'type': 'string', 'maxlength': 3, 'empty': False, 'allowed': ['OUI', 'NON']}, 
               'NO_X': {'empty': False, 'validator': is_float}, 
@@ -92,7 +103,7 @@ class Noeud(BaseGeoModel):
               'NO_PRO': {'type': 'string', 'maxlength': 20, 'empty': False, 'allowed': ['MANCHE NUMERIQUE', 'COLLECTIVITE', 'ORANGE', 'ERDF', 'PRIVE', 'AUTRE (à préciser)', 'NUL']}, 
               'NO_GEST': {'type': 'string', 'maxlength': 20, 'empty': False, 'allowed': ['MANCHE NUMERIQUE', 'MANCHE TELECOM', 'COLLECTIVITE', 'ORANGE', 'ERDF', 'MANCHE FIBRE', 'PRIVE', 'AUTRE (à préciser)', 'NUL']}, 
               'NO_HAUT': {'empty': False, 'validator': is_float}, 
-              'NO_DATE_IN': {'empty': False, 'validator': is_french_date}, 
+              'NO_DATE_IN': {'empty': False, 'validator': is_modern_french_date}, 
               'NO_REF_PLA': {'type': 'string', 'maxlength': 100}, 
               'NO_SRC_GEO': {'type': 'string', 'maxlength': 50}, 
               'NO_QLT_GEO': {'type': 'string', 'maxlength': 1, 'empty': False, 'allowed': ['A', 'B', 'C']}, 
@@ -100,6 +111,9 @@ class Noeud(BaseGeoModel):
               'NO_COMMENT': {'type': 'string', 'maxlength': 300, 'empty': True}, 
               'NO_STATUT': {'type': 'string', 'maxlength': 14, 'empty': False, 'allowed': ['APS', 'APD', 'EXE', 'REC']}}
 
+    def __repr__(self):
+        return "Noeud {}".format(self.NO_NOM)
+    
 class Tranchee(BaseGeoModel):
     filename = "tranchee_geo.shp"
     schema = {'geom': {'geometry': (gis.POLYLINE, (XMIN, YMIN, XMAX, YMAX))}, 
@@ -117,17 +131,22 @@ class Tranchee(BaseGeoModel):
               'TR_PRO_VOI': {'type': 'string', 'empty': False, 'allowed': ['COMMUNE', 'COMMUNAUTE DE COMMUNES', 'DEPARTEMENT', 'ETAT', 'PRIVE']}, 
               'TR_GEST_VO': {'type': 'string', 'empty': False, 'allowed': ['COMMUNE', 'COMMUNAUTE DE COMMUNES', 'DEPARTEMENT', 'ETAT', 'PRIVE']}, 
               'TR_SCHEMA': {'maxlength': 100, 'type': 'string'}, 
-              'TR_DATE_IN': {'empty': False, 'validator': is_french_date}, 
+              'TR_DATE_IN': {'empty': False, 'validator': is_modern_french_date}, 
               'TR_SRC_GEO': {'type': 'string', 'maxlength': 50}, 
               'TR_QLT_GEO': {'type': 'string', 'empty': False, 'allowed': ['A', 'B', 'C']}, 
               'TR_PRO_MD': {'type': 'string', 'maxlength': 20}, 
               'TR_COMMENT': {'type': 'string', 'maxlength': 300, 'empty': True}, 
               'TR_STATUT': {'type': 'string', 'empty': False, 'allowed': ['APS', 'APD', 'EXE', 'REC']}}
 
+    def __repr__(self):
+        return "Tranchee {}".format(self.TR_VOIE)
+    
 class Zapbo(BaseGeoModel):
     filename = "zapbo_geo.shp"
     schema = {'geom': {'geometry': (gis.POLYGON, (XMIN, YMIN, XMAX, YMAX))}, 
-              'ID_ZAPBO': {'type': 'string', 'maxlength': 30}, 
+              'ID_ZAPBO': {'type': 'string', 'maxlength': 30, 'contains_any_of': ['PBO', 'BPE']}, 
               'COMMENTAIR': {'type': 'string', 'maxlength': 254, 'empty': True}, 
               'STATUT': {'type': 'string', 'empty': False, 'allowed': ['APS', 'APD', 'EXE', 'REC']}}
-    
+    
+    def __repr__(self):
+        return "Zapbo {}".format(self.ID_ZAPBO)

+ 28 - 0
schemas/netgeo_1_12_doe/readme.md

@@ -0,0 +1,28 @@
+## Tests effectués
+
+* **Chargement des données**
+
+  * Présence des fichiers attendus: "artere_geo.shp", "cable_geo.shp", "equipement_passif.shp", "noeud_geo.shp", "tranchee_geo.shp","zapbo_geo.shp"
+  * Fichiers au format SHP
+  * Types de géométrie
+
+* **Structure**
+
+  * Les coordonnées des entités sont dans la zone d'emprise du département de la Manche
+  * Les champs attendus sont présents
+  * Les champs obligatoires sont renseignés
+  * Type des données (entiers, décimaux, dates...)
+  * Les champs ne dépassent pas la longueur autorisée
+  * Contrôle des valeurs autorisées
+  
+* **Validation Métier**
+
+  * Contrôle des doubloins dans les codes des noeuds, equipements, zapbo.
+  * Contrôle des cardinalités: correspondance artère/noeuds, cables/equipements, equipement/noeud...etc.
+  * Contrôle des doublons graphiques
+  * Cohérence de la géographie (ex: les noeuds sont situés aux extrémités des artères)
+  * Contrôle de la logique des données (ex: le nombre des fourreaux utilisés n'est pas supérieur au nombre total)
+  * Comparaison avec la base de données (ex: un noeud portant le même code n'existe pas déjà ailleurs sur le territoire)
+  
+
+  

+ 171 - 32
schemas/netgeo_1_12_doe/validator.py

@@ -2,7 +2,9 @@
 
 @author: olivier.massot, 2018
 '''
-from core.validation import NetgeoValidator, DuplicatedPk, RelationError
+from core import mn, gis
+from core.validation import NetgeoValidator, DuplicatedPk, RelationError, \
+    DuplicatedGeom, MissingItem, DimensionError
 from schemas.netgeo_1_12_doe.models import Artere, Cable, Equipement, Noeud, \
     Tranchee, Zapbo
 
@@ -13,61 +15,198 @@ class Netgeo112DoeValidator(NetgeoValidator):
     
     def _technical_validation(self):
         
-        # construction de l'index
-        self.index = {}
-        for model in self.dataset:
-            self.index[model] = {}
-            
-            for item in self.dataset[model]:
-                pk_value = getattr(item, model.pk)
-                if pk_value in self.index[model]:
-                    self.log_error(DuplicatedPk("Doublons dans le champs", filename=model.filename, field=model.pk))
-                else:
-                    self.index[model][pk_value] = item
-            
-        del self.dataset
-             
-        if self.errors: 
-            return
-            
+        # construction des index
+        arteres = self.dataset[Artere]
+        cables = self.dataset[Cable]
+        tranchees = self.dataset[Tranchee]
+        
+        noeuds = {}
+        for noeud in self.dataset[Noeud]:
+            if not noeud.NO_NOM in noeuds:
+                noeuds[noeud.NO_NOM] = noeud
+            else:
+                self.log_error(DuplicatedPk("Doublons dans le champs: {}".format(noeud.NO_NOM), filename=Noeud.filename, field="NO_NOM"))
+        
+        equipements = {}
+        for equipement in self.dataset[Equipement]:
+            if not equipement.EQ_NOM in equipements:
+                equipements[equipement.EQ_NOM] = equipement
+            else:
+                self.log_error(DuplicatedPk("Doublons dans le champs: {}".format(equipement.NO_NOM), filename=Equipement.filename, field="EQ_NOM"))
+                
+        zapbos = {}
+        for zapbo in self.dataset[Zapbo]:
+            if not zapbo.ID_ZAPBO in zapbos:
+                zapbos[zapbo.ID_ZAPBO] = zapbo
+            else:
+                self.log_error(DuplicatedPk("Doublons dans le champs: {}".format(zapbo.NO_NOM), filename=Zapbo.filename, field="ID_ZAPBO"))
+        
+        
         # rattachement les noeuds aux artères     
-        for artere in self.index[Artere]:
+        for artere in arteres:
             try:
-                artere.noeud_a = self.index[Artere][artere.AR_NOEUD_A]
+                artere.noeud_a = noeuds[artere.AR_NOEUD_A]
             except KeyError:
+                artere.noeud_a = None
                 self.log_error(RelationError("Le noeud '{}' n'existe pas".format(artere.AR_NOEUD_A), filename=Artere.filename, field="AR_NOEUD_A"))
                 
             try:
-                artere.noeud_b = self.index[Artere][artere.AR_NOEUD_B]
+                artere.noeud_b = noeuds[artere.AR_NOEUD_B]
             except KeyError:
+                artere.noeud_b = None
                 self.log_error(RelationError("Le noeud '{}' n'existe pas".format(artere.AR_NOEUD_B), filename=Artere.filename, field="AR_NOEUD_A"))
         
         # rattachement des equipements aux cables
-        for cable in self.index[Cable]:
+        for cable in cables:
             try:
-                cable.equipement_a = self.index[Cable][cable.CA_EQ_A]
+                cable.equipement_a = equipements[cable.CA_EQ_A]
             except KeyError:
-                self.log_error(RelationError("L'équipement '{}' n'existe pas".format(cable.AR_NOEUD_A), filename=Cable.filename, field="CA_EQ_A"))
+                cable.equipement_a = None
+                self.log_error(RelationError("L'équipement '{}' n'existe pas".format(cable.CA_EQ_A), filename=Cable.filename, field="CA_EQ_A"))
                 
             try:
-                cable.equipement_b = self.index[Cable][cable.CA_EQ_B]
+                cable.equipement_b = equipements[cable.CA_EQ_B]
             except KeyError:
-                self.log_error(RelationError("L'équipement '{}' n'existe pas".format(cable.AR_NOEUD_B), filename=Cable.filename, field="CA_EQ_B"))
-                
-        if self.errors: 
-            return
+                cable.equipement_b = None
+                self.log_error(RelationError("L'équipement '{}' n'existe pas".format(cable.CA_EQ_B), filename=Cable.filename, field="CA_EQ_B"))
+
+        # rattachement des equipements aux noeuds
+        for equipement in equipements.values():
+            try:
+                equipement.noeud = noeuds[equipement.EQ_NOM_NOE]
+            except KeyError:
+                equipement.noeud = None
+                self.log_error(RelationError("Le noeud '{}' n'existe pas".format(equipement.EQ_NOM_NOE, equipement.EQ_NOM), filename=Equipement.filename, field="EQ_NOM_NOE"))
+
+        # verifie que tous les equipements sont l'equipement B d'au moins un cable
+        equipements_b = [cable.CA_EQ_B for cable in cables]
+        for eq_id in equipements:
+            if not eq_id in equipements_b:
+                self.log_error(RelationError("L'equipement '{}' n'est l'équipement B d'aucun cable".format(noeud.NO_NOM), filename=Equipement.filename, field="EQ_NOM"))
+
+#         if self.critical_happened():
+#             return
+
+        # controle des doublons graphiques
+        for i, tranchee in enumerate(tranchees):
+            for other in tranchees[i+1:]:
+                if gis.same_geom(tranchee.geom, other.geom):
+                    self.log_error(DuplicatedGeom("Une entité graphique est dupliquée".format(tranchee), filename=Tranchee.filename, field="geom"))
+                    
+        for i, artere in enumerate(arteres):
+            for other in arteres[i+1:]:
+                if gis.same_geom(artere.geom, other.geom):
+                    self.log_error(DuplicatedGeom("Une entité graphique est dupliquée ('')".format(artere), filename=Artere.filename, field="geom"))
+                    
+        for i, cable in enumerate(cables):
+            for other in cables[i+1:]:
+                if gis.same_geom(cable.geom, other.geom):
+                    self.log_error(DuplicatedGeom("Une entité graphique est dupliquée ('')".format(cable), filename=Cable.filename, field="geom"))
         
-        # Contrôler dans la base si des éléments portant ces codes existent à des emplacements différents
+        ls_noeuds = list(noeuds.values())
+        for i, noeud in enumerate(ls_noeuds):
+            for other in ls_noeuds[i+1:]:
+                if gis.same_geom(noeud.geom, other.geom):
+                    self.log_error(DuplicatedGeom("Une entité graphique est dupliquée ('')".format(noeud), filename=Noeud.filename, field="geom"))
+        del ls_noeuds
+        
+        ls_zapbos = list(zapbos.values())
+        for i, zapbo in enumerate(ls_zapbos):
+            for other in ls_zapbos[i+1:]:
+                if gis.same_geom(zapbo.geom, other.geom):
+                    self.log_error(DuplicatedGeom("Une entité graphique est dupliquée ('')".format(zapbo), filename=Zapbo.filename, field="geom"))
+        del ls_zapbos
+           
+        # Arteres: comparer la géométrie à celle des noeuds
+        for artere in arteres:
+            point_a, point_b = artere.geom.points[0], artere.geom.points[-1]
+            
+            if not artere.noeud_a or artere.noeud_b:
+                continue
+            
+            if point_a not in (artere.noeud_a.geom.points[0], artere.noeud_b.geom.points[0]):
+                self.log_error(DuplicatedGeom("Pas de noeud aux coordonnées attendues ('')".format(artere), filename=Artere.filename, field="geom"))
+            
+            if point_b not in (artere.noeud_a.geom.points[0], artere.noeud_b.geom.points[0]):
+                self.log_error(DuplicatedGeom("Pas de noeud aux coordonnées attendues ('')".format(artere), filename=Artere.filename, field="geom"))
+        
+        # Cables: comparer la géométrie à celle des equipements (on utilise en fait la geom du noeud correspondant à l'équipement)
+        for cable in cables:
+            point_a, point_b = cable.geom.points[0], cable.geom.points[-1]
+            
+            if not cable.equipement_a or cable.equipement_b:
+                continue
+            
+            if point_a not in (cable.equipement_a.noeud.geom.points[0], cable.equipement_b.noeud.geom.points[0]):
+                self.log_error(DuplicatedGeom("Pas d'equipement aux coordonnées attendues ('')".format(cable), filename=Cable.filename, field="geom"))
+            
+            if point_b not in (cable.equipement_a.noeud.points[0], cable.equipement_b.noeud.geom.points[0]):
+                self.log_error(DuplicatedGeom("Pas d'equipement aux coordonnées attendues ('')".format(cable), filename=Cable.filename, field="geom"))
+        
+        
+        # Verifie que chaque tranchée a au moins une artère
+        for tranchee in tranchees:
+            found = False
+            for artere in arteres:
+                if tranchee.geom.bbox == artere.geom.bbox:
+                    found = True
+                    break
+            if not found:
+                self.log_error(MissingItem("Pas de tranchée correspondant à l'artère ('')".format(artere), filename=Artere.filename, field="-"))
+        
+        # Verifie que chaque cable a au moins une artère (sauf si commentaire contient 'baguette')
+        for cable in cables:
+            found = False
+            if "baguette" in cable.CA_COMMENT.lower():
+                continue
+            for artere in arteres:
+                if tranchee.geom.bbox == artere.geom.bbox:
+                    found = True
+                    break
+            if not found:
+                self.log_error(MissingItem("Pas d'artère correspondante au cable ('')".format(cable), filename=Cable.filename, field="-"))
+        
+        # Verifie que chaque artère a au moins un cable (sauf si commentaire contient un de ces mots 'racco client adductio attente bus 'sans cable'')
+        for tranchee in tranchees:
+            found = False
+            if any(x in cable.CA_COMMENT.lower() for x in ['racco','client','adductio','attente','bus','sans cable']):
+                continue
+            for artere in arteres:
+                if tranchee.geom.bbox == artere.geom.bbox:
+                    found = True
+                    break
+            if not found:
+                self.log_error(MissingItem("Pas de cable correspondant à l'artère ('')".format(artere), filename=Artere.filename, field="-"))
+        
+        # Contrôle des dimensions logiques
+        for artere in arteres:
+            if not int(artere.AR_FOU_DIS) <= int(artere.AR_NB_FOUR):
+                self.log_error(DimensionError("Le nombre de fourreaux disponibles doit être inférieur au nombre total ('')".format(artere), filename=Artere.filename, field="AR_FOU_DIS"))
+        
+        for cable in cables:
+            if not int(cable.CA_NB_FO_U) <= int(artere.CA_NB_FO):
+                self.log_error(DimensionError("Le nombre de fourreaux utilisés doit être inférieur au nombre total ('')".format(cable), filename=Cable.filename, field="CA_NB_FO_U"))
+            if not int(cable.CA_NB_FO_D) <= int(artere.CA_NB_FO):
+                self.log_error(DimensionError("Le nombre de fourreaux disponibles doit être inférieur au nombre total ('')".format(cable), filename=Cable.filename, field="CA_NB_FO_D"))
         
         # Contrôler l'emprise des ZAPBO
         
         
+        # Verifier que chaque equipement de type PBO est contenu dans une zapbo, et que le nom de la zapbo contient le nom de l'equipement
+
+        
+        ### Verifs en base
+        ant_db = mn.ANTDb()
         
+        # Contrôler dans la base si des éléments portant ces codes existent à des emplacements différents
+    
+        # verifier que toutes les prises de la plaque sont inclues dans une zapbo
+    
+    
     
 if __name__ == "__main__":
     from core.constants import MAIN
-    subject = MAIN / "work" / "AXIANS_082AP0_REC_180924.zip"
-#     subject = MAIN / "work" / "STURNO_192AP1_REC_171211_OK"
+    subject = MAIN / "work" / "STURNO_228CP0_APD_180301_OK.zip"
     report = Netgeo112DoeValidator.submit(subject)
     print(report)