Merge pull request 'Ajout du frontend et branchement sur l'API axione' (#9) from johan/frontend into master

Reviewed-on: felix.baylac/axione-elig-test#9
This commit is contained in:
NinjaTrappeur 2022-02-13 18:39:11 +00:00
commit b69ec04aff
16 changed files with 618 additions and 114 deletions

3
.gitignore vendored
View file

@ -1,2 +1,3 @@
__pycache__ __pycache__
/elig-test.ini /elig-test.ini
.vscode

View file

@ -45,3 +45,9 @@ curl -v http://127.0.0.1:5000/addresses/communes?s=29530
# Chercher les voies d'une commune via son code insee # Chercher les voies d'une commune via son code insee
curl -v http://127.0.0.1:5000/addresses/fantoirvoies/29036 curl -v http://127.0.0.1:5000/addresses/fantoirvoies/29036
``` ```
### Jeux de données
#### Insee
Importez ce CSV https://www.data.gouv.fr/fr/datasets/base-officielle-des-codes-postaux/ dans la base de données

View file

@ -1,79 +1,110 @@
import sqlite3 import sqlite3
import sys import sys
import json import json
from .model import Commune,FantoirVoie from .model import Commune, FantoirVoie
import re
# DB with addresses info # DB with addresses info
DB_ADDRESSES_PATH_ENV="DB_ADDRESSES_PATH" DB_ADDRESSES_PATH_ENV = "DB_ADDRESSES_PATH"
DB_ADDRESSES_DEFAULT_PATH="/etc/fantoir.sqlite" DB_ADDRESSES_DEFAULT_PATH = "/etc/fantoir.sqlite"
# Table for insee codes # Table for insee codes
DB_TABLE_INSEE_NAME="insee" DB_TABLE_INSEE_NAME = "insee"
DB_COL_COMMUNE_INSEE="Code_commune_INSEE" DB_COL_COMMUNE_INSEE = "Code_commune_INSEE"
DB_COL_COMMUNE_NAME="Nom_commune" DB_COL_COMMUNE_NAME = "Nom_commune"
DB_COL_COMMUNE_POSTE="Code_postal" DB_COL_COMMUNE_POSTE = "Code_postal"
# Table for Fantoir voies (code Rivoli)
DB_TABLE_FANTOIR_NAME="keyv"
DB_COL_FANTOIR_INSEE="key"
DB_FANTOIR_INSEE_KEY_SUFFIX="keyv:"
# Utility to find an address # Utility to find an address
class AddressFinder: class AddressFinder:
def __init__(self, db_addresses_sqlite_path: str): def __init__(self, db_insee_communes_sqlite_path: str, db_fantoir_voies_sqlite_path: str):
self.dbPath = db_addresses_sqlite_path self.dbCommunesPath = db_insee_communes_sqlite_path
print("DB addresses Path : " + self.dbPath) self.dbFantoirPath = db_fantoir_voies_sqlite_path
print("DB insee communes Path : " + self.dbCommunesPath)
print("DB Fantoir voies Path : " + self.dbFantoirPath)
def getCommunesFromNameOrZip(self, communeNameOrZip: str) -> list[Commune]: def getCommunesFromNameOrZip(self, communeNameOrZip: str, limit: int = None) -> list[Commune]:
con = sqlite3.connect(self.dbPath) con = sqlite3.connect(self.dbCommunesPath)
con.row_factory = sqlite3.Row con.row_factory = sqlite3.Row
cur = con.cursor() cur = con.cursor()
communes: list[Commune] = []
try:
if communeNameOrZip is None:
cur.execute(f"SELECT * from \"{DB_TABLE_INSEE_NAME}\"")
else:
cur.execute(f"SELECT * from \"{DB_TABLE_INSEE_NAME}\" WHERE {DB_COL_COMMUNE_NAME}=\"{communeNameOrZip}\" COLLATE nocase OR {DB_COL_COMMUNE_POSTE}=\"{communeNameOrZip}\"")
except sqlite3.OperationalError as err:
print("Error querying DB : {0}".format(err), file=sys.stderr)
return []
rows = [dict(row) for row in cur.fetchall()]
con.close()
for row in rows:
commune=Commune(
codeInsee=row[DB_COL_COMMUNE_INSEE],
nom=row[DB_COL_COMMUNE_NAME],
codeZip=row[DB_COL_COMMUNE_POSTE])
communes.append(commune)
return communes
def getCommuneFantoirVoies(self, communeInseeCode: str) -> list[FantoirVoie]: # Check if a search limit is specified, make sure it is an integer
select_limit = ""
if limit is not None:
try:
select_limit = f"LIMIT {int(limit)}"
except ValueError:
print("Error, limit arg not a valid int: ", limit)
try:
# If no search parameter, select all
if communeNameOrZip is None:
cur.execute(
f"SELECT * from \"{DB_TABLE_INSEE_NAME}\" {select_limit}")
else:
communeSearch = communeNameOrZip
zipSearch = communeNameOrZip
searchOpertor = "OR"
# Allow search zip and commune at the same time, in the format "29530 PLO"
regexCommuneAndZip = r"[0-9]{5} .+"
if re.match(regexCommuneAndZip, communeNameOrZip):
splitSearch = communeNameOrZip.split(' ')
zipSearch = splitSearch[0]
communeSearch = ' '.join(splitSearch[1:])
searchOpertor = "AND"
cur.execute(
f"SELECT * from \"{DB_TABLE_INSEE_NAME}\" WHERE {DB_COL_COMMUNE_NAME} LIKE \"%{communeSearch}%\" COLLATE nocase {searchOpertor} {DB_COL_COMMUNE_POSTE} LIKE \"{zipSearch}%\" {select_limit}")
except sqlite3.OperationalError as err:
print("Error querying DB : {0}".format(err), file=sys.stderr)
return []
communesMap = dict()
for row in cur.fetchall():
row_obj = dict(row)
commune = Commune(
codeInsee=row_obj[DB_COL_COMMUNE_INSEE],
nom=row_obj[DB_COL_COMMUNE_NAME],
codeZip=row_obj[DB_COL_COMMUNE_POSTE])
# This way we avoid duplicates in DB
communesMap[commune["codeInsee"]] = commune
con.close()
return list(communesMap.values())
def getCommuneFantoirVoies(self, communeInseeCode: str, voieSearch: str = None, limit: int = None) -> list[FantoirVoie]:
# Extract data from DB # Extract data from DB
con = sqlite3.connect(self.dbPath) con = sqlite3.connect(self.dbFantoirPath)
con.row_factory = sqlite3.Row con.row_factory = sqlite3.Row
cur = con.cursor() cur = con.cursor()
if voieSearch is None:
voieSearch=''
# Check if a search limit is specified, make sure it is an integer
select_limit = ""
if limit is not None:
try:
select_limit = f"LIMIT {int(limit)}"
except ValueError:
print("Error, limit arg not a valid int: ", limit)
try: try:
cur.execute(f"SELECT value from \"{DB_TABLE_FANTOIR_NAME}\" WHERE {DB_COL_FANTOIR_INSEE}=\"{DB_FANTOIR_INSEE_KEY_SUFFIX}{communeInseeCode}\"") cur.execute(
f"SELECT trim(libelle), rivoli_with_key from fantoir WHERE full_insee=\"{communeInseeCode}\" AND libelle like \"%{voieSearch}%\" {select_limit}")
except sqlite3.OperationalError as err: except sqlite3.OperationalError as err:
print("Error querying DB : {0}".format(err), file=sys.stderr) print("Error querying DB : {0}".format(err), file=sys.stderr)
return [] return []
data_raw = cur.fetchone() data_raw = cur.fetchall()
con.close() con.close()
## Get JSON payload
fantoir_dict = [] fantoir_dict = []
# Check if data where found # Check if data where found
if data_raw is not None: if data_raw is not None:
data = dict(data_raw) fantoir_dict = dict(data_raw)
# Extract the data behind "value" which is a JSON structure
data_dict=json.loads(data.get("value"))
# In extracted JSON data, the interesting payload is behind "value" key
fantoir_dict = data_dict.get("value")
else: else:
print("Did not found any data matching Insee code " + str(communeInseeCode)) print("Did not found any data matching Insee code " +
str(communeInseeCode))
# Return the json dump # Return the json dump
return fantoir_dict return fantoir_dict

View file

@ -6,12 +6,5 @@ class Commune(TypedDict):
codeZip: str codeZip: str
class FantoirVoie(TypedDict): class FantoirVoie(TypedDict):
id: str libelle: str
dateAjout: int rivoli_with_key: int
libelle: list[str]
typeVoie:str
codeCommune: str
codeFantoir: str
cleRivoli: str
nomCommune: str
predecesseur: bool

View file

@ -23,14 +23,37 @@ def ptoRequest(ptoRef):
</soapenv:Envelope> </soapenv:Envelope>
""" """
def fantoirRequest(insee, rivoli, numVoie):
ts = datetime.now(timezone.utc).isoformat()
return f"""
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:ent="http://structureadresseftth.axione.fr/model/entreprise" xmlns:com="http://structureadresseftth.axione.fr/model/commun">
<soapenv:Header/>
<soapenv:Body>
<ent:obtentionStructureAdresseDemandeSoap>
<ent:entete versionWS="3.0" horodatageRequete="{ts}">
<com:operateurCommercial nom="AQUILENET" identifiant=""/>
</ent:entete>
<ent:referenceAdresse>
<com:referenceRivoli codeInsee="{insee}" codeRivoli="{rivoli}" numeroVoie="{numVoie}"/>
</ent:referenceAdresse>
</ent:obtentionStructureAdresseDemandeSoap>
</soapenv:Body>
</soapenv:Envelope>
"""
def query_axione_fantoir(cfg, insee, rivoli, numVoie):
body = fantoirRequest(insee, rivoli, numVoie)
return query_axione(cfg, body)
def query_axione_pto(cfg, ptoRef): def query_axione_pto(cfg, ptoRef):
body = ptoRequest(ptoRef) body = ptoRequest(ptoRef)
return query_axione(cfg, body)
def query_axione(cfg, body):
# Note: the password should be the base64 of username:password. # Note: the password should be the base64 of username:password.
# Don't ask why. # Don't ask why.
passwd = base64.b64encode(f"{cfg.username}:{cfg.password}".encode("utf8")).decode( passwd = cfg.password
"utf8"
)
headers = { headers = {
"User-Agent": "aquilenet-elig-test/0.1", "User-Agent": "aquilenet-elig-test/0.1",
"Accept": "*/*", "Accept": "*/*",
@ -38,7 +61,7 @@ def query_axione_pto(cfg, ptoRef):
"Connection": "Keep-Alive", "Connection": "Keep-Alive",
"Authorization": passwd, "Authorization": passwd,
} }
resp = None respData = None
if not cfg.debug: if not cfg.debug:
try: try:
conn = http.client.HTTPSConnection( conn = http.client.HTTPSConnection(
@ -67,11 +90,10 @@ def query_axione_pto(cfg, ptoRef):
print(headers) print(headers)
print("BODY: ") print("BODY: ")
print(body) print(body)
print("===================")
with open("./fixtures/dummy-data-1.xml", "r") as f: with open("./fixtures/dummy-data-1.xml", "r") as f:
dummyData = f.read() dummyData = f.read()
return dummyData return dummyData
return resp return respData
class LigneResult(TypedDict): class LigneResult(TypedDict):
@ -98,17 +120,45 @@ class BatimentResult(TypedDict):
referenceBatiment: str referenceBatiment: str
etages: list[EtageResult] etages: list[EtageResult]
class AxioneErreur(TypedDict):
codeErreur: str
libelleErreur: str
def parse_response(resp_str) -> list[BatimentResult]: class AxioneResult(TypedDict):
codeRetour: int
axioneErreur: AxioneErreur
batiments: list[BatimentResult]
def parse_response(resp_str) -> AxioneResult:
root = ET.fromstring(resp_str) root = ET.fromstring(resp_str)
parsedBatiments = [ codeRetourXml = root.find(
parse_batiment(b) ".//{http://structureadresseftth.axione.fr/model/commun}codeRetour"
for b in root.findall( )
".//{http://structureadresseftth.axione.fr/model/commun}batiment" codeRetour = int(codeRetourXml.text.strip())
) axioneErreur = None
] parsedBatiments = []
return parsedBatiments if codeRetour == 0:
parsedBatiments = [
parse_batiment(b)
for b in root.findall(
".//{http://structureadresseftth.axione.fr/model/commun}batiment"
)
]
else:
axioneErreur = AxioneErreur(
codeErreur = root.find(
".//{http://structureadresseftth.axione.fr/model/commun}codeErreur"
).text.strip(),
libelleErreur = root.find(
".//{http://structureadresseftth.axione.fr/model/commun}libelleErreur"
).text.strip()
)
return AxioneResult(
codeRetour=codeRetour,
axioneErreur=axioneErreur,
batiments=parsedBatiments
)
def parse_batiment(batiment) -> BatimentResult: def parse_batiment(batiment) -> BatimentResult:
etatBatiment = batiment.get("etatBatiment", None) etatBatiment = batiment.get("etatBatiment", None)

View file

@ -2,11 +2,12 @@ import configparser
class Config: class Config:
def __init__(self, username, password, source_addr, db_addresses_sqlite_path): def __init__(self, username, password, source_addr, db_insee_communes_sqlite_path, db_fantoir_voies_sqlite_path):
self.username = username self.username = username
self.password = password self.password = password
self.source_addr = source_addr self.source_addr = source_addr
self.db_addresses_sqlite_path = db_addresses_sqlite_path self.db_insee_communes_sqlite_path = db_insee_communes_sqlite_path
self.db_fantoir_voies_sqlite_path = db_fantoir_voies_sqlite_path
self.debug = False self.debug = False
@ -17,5 +18,6 @@ def parse_config(cfgPath):
username = cfg.get("API", "username") username = cfg.get("API", "username")
passwd = cfg.get("API", "password") passwd = cfg.get("API", "password")
source_addr = cfg.get("API", "source_addr") source_addr = cfg.get("API", "source_addr")
db_addresses_sqlite_path = cfg.get("ADDRESSES","db_addresses_sqlite_path") db_insee_communes_sqlite_path = cfg.get("ADDRESSES","db_insee_communes_sqlite_path")
return Config(username, passwd, source_addr,db_addresses_sqlite_path) db_fantoir_voies_sqlite_path = cfg.get("ADDRESSES","db_fantoir_voies_sqlite_path")
return Config(username, passwd, source_addr,db_insee_communes_sqlite_path, db_fantoir_voies_sqlite_path)

View file

@ -5,4 +5,5 @@
# to send the requests from. # to send the requests from.
source_addr = xxx.xxx.xxx.xxx source_addr = xxx.xxx.xxx.xxx
[ADDRESSES] [ADDRESSES]
db_addresses_sqlite_path = path/to/db.sqlite db_insee_communes_sqlite_path = path/to/db.sqlite
db_fantoir_voies_sqlite_path = path/to/db.sqlite

46
fixtures/dummy-data-2.xml Normal file
View file

@ -0,0 +1,46 @@
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/">
<SOAP-ENV:Header />
<SOAP-ENV:Body>
<ns3:obtentionStructureAdresseReponseSoap xmlns:ns2="http://structureadresseftth.axione.fr/model/commun" xmlns:ns3="http://structureadresseftth.axione.fr/model/entreprise">
<ns3:entete horodatageReponse="2022-02-13T00:57:50.773+01:00" horodatageRequete="2022-02-12T23:57:48.974060Z" identifiantReponse="1644710270773" versionWS="3.0">
<ns2:operateurCommercial identifiant="" nom="AQUILENET" />
</ns3:entete>
<ns3:codeRetour>
<ns2:codeRetour>0</ns2:codeRetour>
</ns3:codeRetour>
<ns3:codeOI>BEFO</ns3:codeOI>
<ns3:etatImmeuble>true</ns3:etatImmeuble>
<ns3:structureDetaillee>
<ns2:adresse referenceHexacle="361992226N">
<ns2:referenceRivoli codeInsee="36199" codeRivoli="0031" numeroVoie="48" />
</ns2:adresse>
<ns2:batiment conditionsSyndic="false" dateDebutAcceptationCmdAcces="2021-09-14T00:00:00.000+02:00" dateDebutFournitureCRCmdAcces="2021-09-14T00:00:00.000+02:00" etatBatiment="COMMERCIALISABLE" identifiantImmeuble="BAT36-7C32FE6AD11F4A6797E4F29E" nombreLogementsImmeuble="1" referenceBatiment="NA">
<ns2:referenceGeographique coordonneeImmeubleX="2.027143270510003" coordonneeImmeubleY="47.01243491007431" typeProjection="WGS84" />
<ns2:escalier reference="NA">
<ns2:etage nombreLignesActives="0" nombreLignesExistantes="0" nombreLocauxFTTH="1" refPriseCommandeObligatoire="false" reference="RDC">
<ns2:listePbo>
<ns2:pbo referencePBO="PBO36_ISS1_139" typePbo="AERIEN" />
</ns2:listePbo>
<ns2:listeLignesFTTH>
<ns2:ligneFTTH>
<ns2:prise etiquetteAPoser="Non" referencePBO="PBO36_ISS1_139">
<ns2:referencePTO>BF-ISS1-0110</ns2:referencePTO>
<ns2:referencePrisePromoteur />
<ns2:statutLigneFTTH actif="false" commercialisable="true" existant="false" raccordable="true" rompu="false" />
</ns2:prise>
</ns2:ligneFTTH>
<ns2:ligneFTTH>
<ns2:local localisationLocalOC="" localisationLocalOI="" />
</ns2:ligneFTTH>
</ns2:listeLignesFTTH>
<ns2:pm referencePM="ADR_36199_ISS1" referencePMT="PMT_36199_ISS1">
<ns2:typeEmplacementPM>PME</ns2:typeEmplacementPM>
<ns2:responsableBrassage>OC</ns2:responsableBrassage>
</ns2:pm>
</ns2:etage>
</ns2:escalier>
</ns2:batiment>
</ns3:structureDetaillee>
</ns3:obtentionStructureAdresseReponseSoap>
</SOAP-ENV:Body>
</SOAP-ENV:Envelope>

36
fixtures/dummy-data-3.xml Normal file
View file

@ -0,0 +1,36 @@
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/">
<SOAP-ENV:Header />
<SOAP-ENV:Body>
<ns3:obtentionStructureAdresseReponseSoap xmlns:ns2="http://structureadresseftth.axione.fr/model/commun" xmlns:ns3="http://structureadresseftth.axione.fr/model/entreprise">
<ns3:entete horodatageReponse="2022-02-13T00:55:44.339+01:00" horodatageRequete="2022-02-12T23:55:42.382844Z" identifiantReponse="1644710144339" versionWS="3.0">
<ns2:operateurCommercial identifiant="" nom="AQUILENET" />
</ns3:entete>
<ns3:codeRetour>
<ns2:codeRetour>1</ns2:codeRetour>
<ns2:codeErreur>I01</ns2:codeErreur>
<ns2:libelleErreur>Code Rivoli introuvable, manquant ou incomplet</ns2:libelleErreur>
</ns3:codeRetour>
<ns3:codeOI />
<ns3:structureDetaillee>
<ns2:adresse />
<ns2:batiment referenceBatiment="">
<ns2:referenceGeographique coordonneeImmeubleX="" coordonneeImmeubleY="" typeProjection="" />
<ns2:escalier reference="">
<ns2:etage refPriseCommandeObligatoire="false" reference="">
<ns2:listePbo>
<ns2:pbo referencePBO="" typePbo="" typeRaccoPbPTO="" />
</ns2:listePbo>
<ns2:listeLignesFTTH>
<ns2:ligneFTTH />
</ns2:listeLignesFTTH>
<ns2:pm referencePM="" referencePMT="">
<ns2:typeEmplacementPM>PME</ns2:typeEmplacementPM>
<ns2:responsableBrassage>OI</ns2:responsableBrassage>
</ns2:pm>
</ns2:etage>
</ns2:escalier>
</ns2:batiment>
</ns3:structureDetaillee>
</ns3:obtentionStructureAdresseReponseSoap>
</SOAP-ENV:Body>
</SOAP-ENV:Envelope>

View file

@ -1,3 +1,3 @@
#!/usr/bin/env bash #!/usr/bin/env bash
DEBUG=true CONFIG=./elig-test.ini FLASK_APP=webapp poetry run flask run --reload DEBUG=true TEMPLATES_AUTO_RELOAD=true FLASK_ENV=development CONFIG=./elig-test.ini FLASK_APP=webapp poetry run flask run --reload

29
scripts/import-laposte-insee.sh Executable file
View file

@ -0,0 +1,29 @@
#!/usr/bin/env bash
set -eau -o pipefail
if [ "$#" -ne 2 ]; then
echo "Usage: import-laposte-insee.sh path-to-laposte-insee-CSV fantoir-sqlite-db-path"
echo ""
echo "ERROR: Missing laposte CSV."
echo "You can download it at https://www.data.gouv.fr/fr/datasets/base-officielle-des-codes-postaux/"
exit 1
fi
tmpDir=$(mktemp -d)
clean_tmp () {
rm -r "${tmpDir}"
}
trap clean_tmp EXIT
tmpSql="${tmpDir}"/import-laposte-hexasmal.sql
echo "Importing laposte/insee hexasmal data into the fantoir db."
cat >"${tmpSql}" <<EOF
.separator ";"
.import $1 insee
EOF
sqlite3 "${2}" < "${tmpSql}"
echo "Data imported"

BIN
static/find_pto.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

View file

@ -1,25 +1,235 @@
<!doctype html> <!doctype html>
<html lang="fr"> <html lang="fr">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css"
integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">
<script src="https://code.jquery.com/jquery-3.3.1.slim.min.js"
integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.7/umd/popper.min.js"
integrity="sha384-UO2eT0CpHqdSJQ6hJty5KVphtPhzWj9WO1clHTMGa3JDZwrnQq4sF86dIHNDz0W1"
crossorigin="anonymous"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js"
integrity="sha384-JjSmVgyd0p3pXB1rRibZUAYoIIy6OrQ6VrjIEaFf/nJGzIxFDsf4x0xIM+B07jRM"
crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/@tarekraafat/autocomplete.js@10.2.6/dist/autoComplete.min.js"></script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@tarekraafat/autocomplete.js@10.2.6/dist/css/autoComplete.02.min.css">
<title>Aquilenet: Éligibilité FTTH</title> <title>Aquilenet: Éligibilité FTTH</title>
<style> <style>
{% include 'style.css' %} {% include 'style.css'%}
</style> </style>
</head> </head>
<body> <body>
<h1 id="aquilenet-title">Aquilenet</h1> <h1 class="text-center">
<div id="container"> <a href="https://www.aquilenet.fr/" id="aquilenet-title">AQUILENET</a>
<h1 id="main-title">Test d'Éligibilité FTTH Aquilenet</h1> </h1>
<form method="post" action="/result"> <div id="mainContainer" class="container">
<label>Numéro de PTO :
<input name="pto"/> <div class="row d-flex justify-content-between align-items-center" data-parent="#mainContainer">
</label>
<button>Tester</button> <div class="col-12 text-intro">
</form> <div id="methodPto" class="collapse show testMethods">
<h2 class="text-center form-title">Test d'éligibilité par PTO</h2>
<form id="formPtoTest" method="post" action="/test/pto">
<div class="form-group" id="ptoForm">
<label class="form-label" for="pto-number">Numéro PTO</label>
<input autofocus required type="text" class="form-control" name="pto" id="pto-number" aria-describedby="ptoHelp"
placeholder="OOOO-XXXX-XXXX" oninvalid="this.setCustomValidity('Veuillez renseigner le PTO')"
oninput="setCustomValidity('')">
<small id="ptoHelp" class="form-text btn btn-link" data-toggle="collapse" data-target="#ptoInfo"
aria-expanded="true" aria-controls="ptoInfo">Où trouver mon numéro de PTO ?</small>
<div id="ptoInfo" class="collapse" aria-labelledby="ptoForm" data-parent="#ptoForm">
<div class="card-body">
PTO (Point de terminaison optique) est un numéro unique que vous pouvez trouver sur le boîtier de
raccordement de la fibre.
C'est un petit boîtier blanc installé dans la maison
<br>
<img src="{{url_for('static', filename='find_pto.jpg')}}" class="img-fluid" alt="Responsive image">
</div>
</div>
</div>
<button id="buttonPtoTest" type="submit" class="btn btn-sable">Tester le PTO</button>
</form>
<br>
<button id="buttonNoPto" type="button" data-toggle="collapse" data-target=".testMethods"
class="btn btn-link page-nav-btn" aria-expanded="false" aria-controls="methodPto methodAddress">Je n'ai pas/ne trouve
pas le PTO, tester autrement</button>
</div>
<div id="methodAddress" class="collapse testMethods">
<h2 class="text-center form-title">Test d'éligibilité par adresse</h2>
<form id="formAddressTest" method="post" action="/test/address">
<div class="form-group row" id="communeForm">
<label class="form-label col-sm-2 my-1" for="commune-autocomplete">Commune</label>
<input id="commune-autocomplete" class="col-sm-9" type="search" dir="ltr" spellcheck=false autocorrect="off" autocomplete="off" class="form-control" autocapitalize="off"/>
</div>
<div class="form-row collapse" id="voieForm">
<div class="col-sm-3 my-1">
<label class="form-label" for="numeroVoieInput">Numéro de voie</label>
<input required type="text" name="numeroVoie" class="form-control" id="numeroVoieInput"
aria-describedby="numeroVoieHelp" placeholder="Numéro de voie"
oninvalid="this.setCustomValidity('Veuillez renseigner le numéro de voie')"
oninput="setCustomValidity('')">
</div>
<div class="col-sm-7 my-1">
<label class="form-label" for="fantoir-autocomplete">Nom de voie</label>
<input id="fantoir-autocomplete" class="form-control" type="search" dir="ltr" spellcheck=false autocorrect="off" autocomplete="off" autocapitalize="off"/>
</div>
</div>
<br>
<button id="btnTestAdresse" type="submit" class="btn btn-sable collapse">Tester l'adresse</button>
</form>
<br>
<button id="buttonReturnPto" type="button" data-toggle="collapse" data-target=".testMethods"
class="btn btn-link page-nav-btn" aria-expanded="false" aria-controls="methodPto methodAddress">Revenir au test par
PTO</button>
</div>
</div>
</div>
</div> </div>
<script>
$(document).ready(function () {
// AbortController allows to cancel promises
const controllerCommunes = new AbortController();
const { signalCommunes } = controllerCommunes;
const controllerVoies = new AbortController();
const { signalVoies } = controllerVoies;
// Global variables
var communes = []
var codeInsee = 0
var codeRivoli = ""
var voies = []
var voie = ""
function sanitizeInputStr(inputStr) {
inputStr = inputStr.replace(/[ÀÁÂÃÄÅàáâãäå]/g, "a");
inputStr = inputStr.replace(/[Ææ]/g, "ae");
inputStr = inputStr.replace(/[Çç]/g, "c");
inputStr = inputStr.replace(/[ÈÉÊËèéêë]/g, "e");
inputStr = inputStr.replace(/[ÌÍÎÏìíîï]/g, "i");
inputStr = inputStr.replace(/[Ññ]/g, "n");
inputStr = inputStr.replace(/[ÒÓÔÕÖòóôõö]/g, "o");
inputStr = inputStr.replace(/[ÙÚÛÜùúûü]/g, "u");
inputStr = inputStr.replace(/[Ýý]/g, "y");
inputStr = inputStr.replace(/[ß]/g, "ss");
return inputStr;
}
function setButtonSpinner(buttonSelector, buttonText) {
// disable button
$(buttonSelector).prop("disabled", true);
// add spinner to button
$(buttonSelector).html(
`<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>${buttonText}`
);
}
$('#methodAddress').on('show.bs.collapse', function () {
$('#communeInput').trigger('input')
$('#communeInput').trigger('keyup')
});
$('#methodAddress').on('shown.bs.collapse', function () {
$('#communeInput').focus();
});
$('#voieForm').on('show.bs.collapse', function () {
$('#voieInput').trigger('input')
$('#voieInput').trigger('keyup')
});
$('#voieForm').on('shown.bs.collapse', function () {
$('#numeroVoieInput').focus();
});
const autoCompleteCommune = new autoComplete({
selector: "#commune-autocomplete",
placeHolder: "Code postal/nom de commune...",
data: {
src: async (query) => {
const api = "addresses/communes?limit=100";
const reqUrl = query === '' ? api : api + "&s=" + query
const source = await fetch(reqUrl, { signalCommunes });
const data = await source.json();
return data;
},
keys: [ "codeZip", "nom" ],
},
resultList: {
element: (list, data) => {
}
},
resultItem: {
highlight: true
},
debounce: 300,
events: {
input: {
selection: (event) => {
const selection = event.detail.selection.value;
autoCompleteCommune.input.value = selection.codeZip + " - " + selection.nom ;
codeInsee = selection.codeInsee;
$("#voieForm").collapse("show");
}
}
}
});
const autoCompleteFantoir = new autoComplete({
selector: "#fantoir-autocomplete",
placeHolder: "Nom de la voie...",
data: {
src: async (query) => {
const api = "addresses/fantoirvoies/" + codeInsee + "?limit=100";
const reqUrl = query === '' ? api : api + "&s=" + query
const source = await fetch(reqUrl, { signalVoies });
const data = await source.json();
return Object.entries(data).map(e => {return {"name": e[0], "value": e[1]}; });
},
keys: [ "name" ],
},
resultList: {
element: (list, data) => {
}
},
resultItem: {
highlight: true
},
debounce: 300,
events: {
input: {
selection: (event) => {
const selection = event.detail.selection.value;
autoCompleteFantoir.input.value = selection.name;
codeRivoli = selection.value;
$('#btnTestAdresse').collapse('show');
}
}
}
});
$('#formAddressTest').submit(function(eventObj) {
setButtonSpinner("#btnTestAdresse", "Test...");
$('#formAddressTest').append(`<input type="hidden" class="form-control" name="codeInsee" value="${codeInsee}" />`);
$('#formAddressTest').append(`<input type="hidden" class="form-control" name="codeRivoli" value="${codeRivoli}" />`);
return true;
});
$('#formPtoTest').submit(function(eventObj) {
setButtonSpinner("#buttonPtoTest", "Test...");
return true;
});
});
</script>
</body> </body>
</html> </html>

View file

@ -3,7 +3,19 @@
<html lang="fr"> <html lang="fr">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css"
integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">
<script src="https://code.jquery.com/jquery-3.3.1.slim.min.js"
integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.7/umd/popper.min.js"
integrity="sha384-UO2eT0CpHqdSJQ6hJty5KVphtPhzWj9WO1clHTMGa3JDZwrnQq4sF86dIHNDz0W1"
crossorigin="anonymous"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js"
integrity="sha384-JjSmVgyd0p3pXB1rRibZUAYoIIy6OrQ6VrjIEaFf/nJGzIxFDsf4x0xIM+B07jRM"
crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/@tarekraafat/autocomplete.js@10.2.6/dist/autoComplete.min.js"></script>
<title>Aquilenet: Éligibilité FTTH</title> <title>Aquilenet: Éligibilité FTTH</title>
<style> <style>
{% include 'style.css' %} {% include 'style.css' %}
@ -11,11 +23,20 @@
</head> </head>
<body> <body>
<h1 id="aquilenet-title">Aquilenet</h1> <h1 class="text-center">
<div id="container"> <a href="https://www.aquilenet.fr/" id="aquilenet-title">AQUILENET</a>
</h1>
<div id="containerResults" class="container">
<a id="buttonReturnTest" type="button" class="btn btn-link page-nav-btn" href="/">
Retour test
</a>
{% if result['codeRetour'] == 0 %}
<h1 id="main-title">Test d'Éligibilité FTTH Aquilenet: Résultats</h1> <h1 id="main-title">Test d'Éligibilité FTTH Aquilenet: Résultats</h1>
<p>Résultat pour le PTO: {{ pto }}</p> <p>Résultat pour le PTO: {{ pto }}</p>
{% for batiment in result %} {% for batiment in result['batiments'] %}
<table> <table>
<thead> <thead>
<tr> <tr>
@ -57,6 +78,12 @@
{% endfor %} {% endfor %}
</table> </table>
{% endfor %} {% endfor %}
{% else %}
<h1 id="main-title">Erreur</h1>
<p>Code d'erreur: {{ result['axioneErreur']['codeErreur'] }}</p>
<p>Description de l'erreur: {{ result['axioneErreur']['libelleErreur'] }}</p>
{% endif %}
</div> </div>
</body> </body>
</html> </html>

View file

@ -1,16 +1,71 @@
html, body {
height : 100%;
/* font-size: 20px; */
}
body { body {
background-color: #1787c2; background-color: #1787c2;
display: flex; /* display: flex;
flex-direction: column; flex-direction: column; */
font-family: 'Titillium Web', sans-serif; font-family: 'Titillium Web', sans-serif;
} }
#aquilenet-title { #aquilenet-title {
color: white; color: whitesmoke;
align-self: center; align-self: center;
font-size: 4em; font-size: 2em;
margin-top: .3em;
margin-bottom: .5em;
text-decoration: none;
}
.form-title {
color: whitesmoke;
align-self: center;
font-size: 2em;
margin-top: .3em; margin-top: .3em;
margin-bottom: .5em; margin-bottom: .5em;
} }
.form-label, .card-body, #containerResults {
color: whitesmoke;
}
#ptoHelp {
color: rgb(192, 192, 192);
text-decoration: underline;
}
.sable .btn-ciel, .ciel .btn-sable{
padding: 8px;
border-radius: 10px;
box-shadow: 3px 3px 3px rgba(0,0,0,.3);
font-size: 1.1em
}
.sable .btn-ciel:hover, .ciel .btn-sable:hover{
box-shadow: none;
}
.page-nav-btn {
color: whitesmoke;
text-decoration: underline;
}
.btn-sable:hover {
background-position: right center; /* change the direction of the change here */
}
.btn-sable {
/* box-shadow: 0 0 20px #eee; */
/* border-radius: 10px; */
color: darkslategray;
transition: 0.5s;
background-size: 200% auto;
text-shadow: dimgray 0 1px 1px;
background-image: linear-gradient(to bottom, #ffd38c 0%, #ffedd0 51%, #e0e0e0 100%);
}
/* Autocomplete */
/* #fda085 51%, */
/*
#container { #container {
width: 80%; width: 80%;
background-color: #ffd38c; background-color: #ffd38c;
@ -29,7 +84,7 @@ form {
align-self: center; align-self: center;
margin-bottom: 2em; margin-bottom: 2em;
} }
*/
table,td { table,td {
border: 2px solid #333; border: 2px solid #333;
border-collapse: collapse; border-collapse: collapse;

View file

@ -3,7 +3,7 @@ from flask import Flask, render_template, request, escape
import json import json
from config.config import parse_config from config.config import parse_config
from axione_api.api import query_axione_pto, parse_response from axione_api.api import query_axione_pto, parse_response, query_axione_fantoir
from address_finder.api import AddressFinder from address_finder.api import AddressFinder
def load_config(): def load_config():
@ -22,7 +22,7 @@ def load_config():
cfg = load_config() cfg = load_config()
addressFinder = AddressFinder(cfg.db_addresses_sqlite_path) addressFinder = AddressFinder(cfg.db_insee_communes_sqlite_path, cfg.db_fantoir_voies_sqlite_path)
app = Flask(__name__) app = Flask(__name__)
@ -30,17 +30,11 @@ app = Flask(__name__)
def get_form(): def get_form():
return render_template("landing_form.html") return render_template("landing_form.html")
@app.route("/result", methods=['POST'])
def show_result():
pto = escape(request.form['pto'])
result = parse_response(query_axione_pto(cfg, pto))
return render_template("result.html", pto=pto, result=result)
@app.route("/addresses/communes", methods=['GET']) @app.route("/addresses/communes", methods=['GET'])
def get_communes(): def get_communes():
to_search=request.args.get('s') to_search=request.args.get('s')
print(to_search) limit=request.args.get('limit')
communes=addressFinder.getCommunesFromNameOrZip(to_search) communes=addressFinder.getCommunesFromNameOrZip(to_search,limit)
response = app.response_class( response = app.response_class(
response=json.dumps(communes), response=json.dumps(communes),
mimetype='application/json' mimetype='application/json'
@ -49,9 +43,32 @@ def get_communes():
@app.route("/addresses/fantoirvoies/<codeInsee>", methods=['GET']) @app.route("/addresses/fantoirvoies/<codeInsee>", methods=['GET'])
def get_fantoir_voies(codeInsee): def get_fantoir_voies(codeInsee):
fantoirVoies=addressFinder.getCommuneFantoirVoies(codeInsee) to_search=request.args.get('s')
limit=request.args.get('limit')
fantoirVoies=addressFinder.getCommuneFantoirVoies(codeInsee,to_search,limit)
response = app.response_class( response = app.response_class(
response=json.dumps(fantoirVoies), response=json.dumps(fantoirVoies),
mimetype='application/json' mimetype='application/json'
) )
return response return response
@app.route("/test/address", methods=['POST'])
def test_address():
codeInsee = escape(request.form['codeInsee'])
numeroVoie = escape(request.form['numeroVoie'])
# Trimming rivoli's key
codeRivoli = escape(request.form['codeRivoli'])[:-1]
result = parse_response(query_axione_fantoir(cfg, codeInsee, codeRivoli, numeroVoie))
if cfg.debug:
print("===================")
print("Dummy response: ")
print(result)
print("===================")
return render_template("result.html", pto="", result=result)
@app.route("/test/pto", methods=['POST'])
def test_pto():
pto = escape(request.form['pto'])
result = parse_response(query_axione_pto(cfg, pto))
return render_template("result.html", pto=pto, result=result)