From 21c5401e77950d27a688532b3dcd87f51f5419b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89loi=20Rivard?= Date: Wed, 6 Dec 2023 16:53:35 +0100 Subject: [PATCH 1/4] refactor: extract constants in a dedicated file --- web/flaskr/constants.py | 281 +++++++++++++++++++++++++++++++++++++++ web/instance/config.py | 283 +--------------------------------------- 2 files changed, 283 insertions(+), 281 deletions(-) create mode 100644 web/flaskr/constants.py diff --git a/web/flaskr/constants.py b/web/flaskr/constants.py new file mode 100644 index 00000000..2e601adf --- /dev/null +++ b/web/flaskr/constants.py @@ -0,0 +1,281 @@ +DEFAULT_EMAIL_WHITELIST = [ + r".*@(.*\.|)ac-aix-marseille\.fr$", + r".*@(.*\.|)ac-amiens\.fr$", + r".*@(.*\.|)ac-besancon\.fr$", + r".*@(.*\.|)ac-bordeaux\.fr$", + r".*@(.*\.|)ac-caen\.fr$", + r".*@(.*\.|)ac-clermont\.fr$", + r".*@(.*\.|)ac-cned\.fr$", + r".*@(.*\.|)ac-corse\.fr$", + r".*@(.*\.|)ac-creteil\.fr$", + r".*@(.*\.|)ac-dijon\.fr$", + r".*@(.*\.|)ac-grenoble\.fr$", + r".*@(.*\.|)ac-guadeloupe\.fr$", + r".*@(.*\.|)ac-guyane\.fr$", + r".*@(.*\.|)ac-lille\.fr$", + r".*@(.*\.|)ac-limoges\.fr$", + r".*@(.*\.|)ac-lyon\.fr$", + r".*@(.*\.|)ac-martinique\.fr$", + r".*@(.*\.|)ac-mayotte\.fr$", + r".*@(.*\.|)ac-montpellier\.fr$", + r".*@(.*\.|)ac-nancy-metz\.fr$", + r".*@(.*\.|)ac-nantes\.fr$", + r".*@(.*\.|)ac-nice\.fr$", + r".*@(.*\.|)ac-normandie\.fr$", + r".*@(.*\.|)ac-noumea\.nc$", + r".*@(.*\.|)ac-orleans-tours\.fr$", + r".*@(.*\.|)ac-paris\.fr$", + r".*@(.*\.|)ac-poitiers\.fr$", + r".*@(.*\.|)ac-polynesie\.pf$", + r".*@(.*\.|)ac-reims\.fr$", + r".*@(.*\.|)ac-rennes\.fr$", + r".*@(.*\.|)ac-reunion\.fr$", + r".*@(.*\.|)ac-rouen\.fr$", + r".*@(.*\.|)ac-spm\.fr$", + r".*@(.*\.|)ac-strasbourg\.fr$", + r".*@(.*\.|)ac-toulouse\.fr$", + r".*@(.*\.|)ac-versailles\.fr$", + r".*@(.*\.|)ac-wf\.wf$", + r".*@(.*\.|)acnusa\.fr$", + r".*@(.*\.|)acte-etat-civil\.fr$", + r".*@(.*\.|)ademe\.fr$", + r".*@(.*\.|)aefe\.fr$", + r".*@(.*\.|)afd\.fr$", + r".*@(.*\.|)agencebio\.org$", + r".*@(.*\.|)agence-regionale-sante\.fr$", + r".*@(.*\.|)anfr\.fr$", + r".*@(.*\.|)anses\.fr$", + r".*@(.*\.|)ansm\.sante\.fr$", + r".*@(.*\.|)aphp\.fr$", + r".*@(.*\.|)apij-justice\.fr$", + r".*@(.*\.|)arcep\.fr$", + r".*@(.*\.|)ars\.sante\.fr$", + r".*@(.*\.|)asi-aeroports\.fr$", + r".*@(.*\.|)asn\.fr$", + r".*@(.*\.|)asp-public\.fr$", + r".*@(.*\.|)assemblee-afe\.fr$", + r".*@(.*\.|)assurance-maladie\.fr$", + r".*@(.*\.|)attachefiscal\.org$", + r".*@(.*\.|)autorite-statistique-publique\.fr$", + r".*@(.*\.|)autoritedelaconcurrence\.fr$", + r".*@(.*\.|)bea\.aero$", + r".*@(.*\.|)biomedecine\.fr$", + r".*@(.*\.|)bnf\.fr$", + r".*@bnu\.fr$", + r".*@(.*\.|)businessfrance\.fr$", + r".*@(.*\.|)cabinet\.education\.fr$", + r".*@(.*\.|)cades\.fr$", + r".*@(.*\.|)ccomptes\.fr$", + r".*@(.*\.|)ccsp\.fr$", + r".*@cea\.fr$", + r".*@(.*\.|)cerema\.fr$", + r".*@(.*\.|)ch-bagneres\.fr$", + r".*@(.*\.|)ch-fidesien\.fr$", + r".*@(.*\.|)ch-lannemezan\.fr$", + r".*@(.*\.|)ch-lourdes\.fr$", + r".*@(.*\.|)ch-tarbes-vic\.fr$", + r".*@(.*\.|)chateauversailles\.fr$", + r".*@(.*\.|)chr-metz-thionville\.fr$", + r".*@chu-amiens\.fr$", + r".*@chu-montpellier\.fr$", + r".*@(.*\.|)chu-nimes\.fr$", + r".*@(.*\.|)cirad\.fr$", + r".*@(.*\.|)cnccep\.fr$", + r".*@(.*\.|)cncdh\.fr$", + r".*@(.*\.|)cnctr\.fr$", + r".*@(.*\.|)cndaspe\.fr$", + r".*@cne2\.fr$", + r".*@(.*\.|)cnil\.fr$", + r".*@(.*\.|)cnis\.fr$", + r".*@(.*\.|)cnpf\.fr$", + r".*@(.*\.|)cnr-elysee\.fr$", + r".*@(.*\.|)comite-du-label\.fr$", + r".*@(.*\.|)comite-du-secret\.fr$", + r".*@(.*\.|)commission-refugies\.fr$", + r".*@(.*\.|)conseil-concurrence\.fr$", + r".*@(.*\.|)conseil-etat\.fr$", + r".*@(.*\.|)cor-retraites\.fr$", + r".*@cre\.fr$", + r".*@crenau\.archi\.fr$", + r".*@(.*\.|)csa\.fr$", + r".*@(.*\.|)csnp\.fr$", + r".*@(.*\.|)culture\.fr$", + r".*@(.*\.|)debatpublic\.fr$", + r".*@(.*\.|)defenseurdesdroits\.fr$", + r".*@(.*\.|)dialogue-trianon\.fr$", + r".*@(.*\.|)ecoledulouvre\.fr$", + r".*@(.*\.|)efs\.sante\.fr$", + r".*@(.*\.|)elysee\.fr$", + r".*@(.*\.|)enac\.fr$", + r".*@(.*\.|)enim\.eu$", + r".*@ensai\.fr$", + r".*@(.*\.|)enssib\.fr$", + r".*@(.*\.|)ensta-paristech\.fr$", + r".*@(.*\.|)epaf\.asso\.fr$", + r".*@(.*\.|)epms-le-littoral\.net$", + r".*@(.*\.|)epms-le-littoral\.org$", + r".*@(.*\.|)erafp\.fr$", + r".*@(.*\.|)espace.gouv\.fr$", + r".*@(.*\.|)europol\.europa\.eu$", + r".*@(.*\.|)europolhq\.net$", + r".*@(.*\.|)fete-gastronomie\.fr$", + r".*@(.*\.|)fnap-logement\.fr$", + r".*@(.*\.|)fr\.europol\.net$", + r".*@(.*\.|)franceagrimer\.fr$", + r".*@(.*\.|)francemobilites\.fr$", + r".*@(.*\.|)frenchmobility\.fr$", + r".*@fun-mooc\.fr$", + r".*@(.*\.|)gouv\.fr$", + r".*@(.*\.|)guimet\.fr$", + r".*@(.*\.|)granddebat\.fr$", + r".*@(.*\.|)has-sante\.fr$", + r".*@(.*\.|)hautconseilclimat\.fr$", + r".*@(.*\.|)hautconseildesbiotechnologies\.fr$", + r".*@(.*\.|)hceres\.fr$", + r".*@(.*\.|)hcf-famille\.fr$", + r".*@(.*\.|)hcfp\.fr$", + r".*@(.*\.|)hebergement2\.interieur-gouv\.fr$", + r".*@(.*\.|)hebergement\.interieur-gouv\.fr$", + r".*@(.*\.|)hopital-le-montaigu\.com$", + r".*@(.*\.|)i-carre\.net$", + r".*@ibcp\.fr$", + r".*@(.*\.|)idda13\.fr$", + r".*@(.*\.|)ifce\.fr$", + r".*@(.*\.|)ign\.fr$", + r".*@ihedn\.fr$", + r".*@(.*\.|)ihest\.fr$", + r".*@(.*\.|)inha\.fr$", + r".*@(.*\.|)inhesj\.fr$", + r".*@(.*\.|)injep\.fr$", + r".*@(.*\.|)inp\.fr$", + r".*@(.*\.|)inpi\.fr$", + r".*@(.*\.|)inra\.fr$", + r".*@(.*\.|)inrae\.fr$", + r".*@(.*\.|)inrap\.fr$", + r".*@(.*\.|)inria\.fr$", + r".*@(.*\.|)insee\.fr$", + r".*@(.*\.|)insep\.fr$", + r".*@(.*\.|)institutcancer\.fr$", + r".*@(.*\.|)ints\.fr$", + r".*@iralille\.fr$", + r".*@ira-lille\.fr$", + r".*@(.*\.|)irisa\.fr$", + r".*@(.*\.|)irstea\.fr$", + r".*@(.*\.|)juradm\.fr$", + r".*@(.*\.|)justice\.fr$", + r".*@(.*\.|)ladocumentationfrancaise\.fr$", + r".*@(.*\.|)loria\.fr$", + r".*@(.*\.|)louvre\.fr$", + r".*@(.*\.|)medecine-de-proximite\.fr$", + r".*@(.*\.|)meteofrance\.fr$", + r".*@(.*\.|)mrccfr\.eu$", + r".*@(.*\.|)mrscfr\.eu$", + r".*@(.*\.|)mucem\.org$", + r".*@(.*\.|)musee-orangerie\.fr$", + r".*@(.*\.|)musee-orsay\.fr$", + r".*@(.*\.|)museepicassoparis.fr$", + r".*@(.*\.|)musee-rodin\.fr$", + r".*@(.*\.|)nancy\.archi\.fr$", + r".*@nantes\.archi$", + r".*@nantes\.archi\.fr$", + r".*@(.*\.|)notification\.service-public\.fr$", + r".*@(.*\.|)odeadom\.fr$", + r".*@(.*\.|)ofgl\.fr$", + r".*@(.*\.|)ofii\.fr$", + r".*@(.*\.|)onf\.fr$", + r".*@(.*\.|)oniam\.fr$", + r".*@parcoursup\.fr$", + r".*@(.*\.|)pibude\.com$", + r".*@(.*\.|)point-info-famille\.fr$", + r".*@(.*\.|)pointinfofamille\.fr$", + r".*@(.*\.|)region-academique-aura\.fr$", + r".*@(.*\.|)region-academique-auvergne-rhone-alpes\.fr$", + r".*@(.*\.|)region-academique-bfc\.fr$", + r".*@(.*\.|)region-academique-bourgogne-franche-comte\.fr$", + r".*@(.*\.|)region-academique-bretagne\.fr$", + r".*@(.*\.|)region-academique-centre-val-de-loire\.fr$", + r".*@(.*\.|)region-academique-corse\.fr$", + r".*@(.*\.|)region-academique-grand-est\.fr$", + r".*@(.*\.|)region-academique-guadeloupe\.fr$", + r".*@(.*\.|)region-academique-guyane\.fr$", + r".*@(.*\.|)region-academique-hauts-de-france\.fr$", + r".*@(.*\.|)region-academique-idf\.fr$", + r".*@(.*\.|)region-academique-ile-de-france\.fr$", + r".*@(.*\.|)region-academique-martinique\.fr$", + r".*@(.*\.|)region-academique-mayotte\.fr$", + r".*@(.*\.|)region-academique-normandie\.fr$", + r".*@(.*\.|)region-academique-nouvelle-aquitaine\.fr$", + r".*@(.*\.|)region-academique-occitanie\.fr$", + r".*@(.*\.|)region-academique-paca\.fr$", + r".*@(.*\.|)region-academique-pays-de-la-loire\.fr$", + r".*@(.*\.|)region-academique-provence-alpes-cote-dazur\.fr$", + r".*@(.*\.|)region-academique-reunion\.fr$", + r".*@(.*\.|)regis-dgac\.net$", + r".*@renater\.fr$", + r".*@(.*\.|)santepubliquefrance\.fr$", + r".*@(.*\.|)service-eco\.fr$", + r".*@(.*\.|)service-public\.fr$", + r".*@(.*\.|)service-public\.fr\.preprod\.ext\.dila\.fr$", + r".*@(.*\.|)service-public\.fr\.qualif\.ext\.dila\.fr$", + r".*@(.*\.|)sevresciteceramique\.fr$", + r".*@(.*\.|)shom\.fr$", + r".*@societedugrandparis\.fr$", + r".*@(.*\.|)taaf\.fr$", + r".*@(.*\.|)telerecours\.fr$", + r".*@(.*\.|)theatre-odeon\.fr$", + r".*@(.*\.|)ugap\.fr$", + r".*@(.*\.|)unedic\.fr$", + r".*@univ-orleans\.fr$", + r".*@(.*\.|)univ-paris13\.fr$", + r".*@univ-perp\.fr$", + r".*@(.*\.|)vie-publique\.fr$", + r".*@(.*\.|)vnf\.fr$", + r".*@univ-ubs\.fr$", + r".*@sdis(?!00|20|69|75|96|97|98|99)[0-9]{2}.fr$", + r".*@sdis-vendee.fr$", + r".*@sdis21.org$", + r".*@sdis36.org$", + r".*@sdis67.com$", + r".*@sdis86.net$", + r".*@sdis97[1-3].fr$", + r".*@sdis976.fr$", + r".*@sdis974.re$", + r".*@(.*\.|)intranet-sdis11\.fr$", + r".*@pompiersparis\.fr$", + r".*@chu-angers\.fr$", + r".*@ensc-rennes\.fr$", + r".*@educagri\.fr$", + r".*@fiva\.fr$", + r".*@assurance-maladie\.fr$", + r".*@crous-aix-marseille\.fr$", + r".*@crous-amiens\.fr$", + r".*@crous-antillesguyane\.fr$", + r".*@crous-bordeaux\.fr$", + r".*@crous-bfc\.fr$", + r".*@crous-clermont\.fr$", + r".*@crous-corse\.fr$", + r".*@crous-creteil\.fr$", + r".*@crous-grenoble\.fr$", + r".*@crous-reunion\.fr$", + r".*@crous-lille\.fr$", + r".*@crous-limoges\.fr$", + r".*@crous-lorraine\.fr$", + r".*@crous-lyon\.fr$", + r".*@crous-montpellier\.fr$", + r".*@crous-nantes\.fr$", + r".*@crous-nice\.fr$", + r".*@crous-normandie\.fr$", + r".*@crous-orleans-tours\.fr$", + r".*@crous-paris\.fr$", + r".*@crous-poitiers\.fr$", + r".*@crous-reims\.fr$", + r".*@crous-strasbourg\.fr$", + r".*@crous-versailles\.fr$", + r".*@crous-rennes\.fr$", + r".*@crous-toulouse\.fr$", + r".*@(.*\.|)assemblee-nationale\.fr$", + r".*@(.*\.|)senat\.fr$", + r".*@ird.fr$", + r".*@ch-chatillon.fr$", + r".*@ch-buzancais.fr$", +] diff --git a/web/instance/config.py b/web/instance/config.py index 87b08061..a7073361 100755 --- a/web/instance/config.py +++ b/web/instance/config.py @@ -2,6 +2,7 @@ import os from flask_babel import lazy_gettext +from flaskr.constants import DEFAULT_EMAIL_WHITELIST # App configuration @@ -300,284 +301,4 @@ BABEL_TRANSLATION_DIRECTORIES = "/opt/bbb-visio/translations" -EMAIL_WHITELIST = [ - r".*@(.*\.|)ac-aix-marseille\.fr$", - r".*@(.*\.|)ac-amiens\.fr$", - r".*@(.*\.|)ac-besancon\.fr$", - r".*@(.*\.|)ac-bordeaux\.fr$", - r".*@(.*\.|)ac-caen\.fr$", - r".*@(.*\.|)ac-clermont\.fr$", - r".*@(.*\.|)ac-cned\.fr$", - r".*@(.*\.|)ac-corse\.fr$", - r".*@(.*\.|)ac-creteil\.fr$", - r".*@(.*\.|)ac-dijon\.fr$", - r".*@(.*\.|)ac-grenoble\.fr$", - r".*@(.*\.|)ac-guadeloupe\.fr$", - r".*@(.*\.|)ac-guyane\.fr$", - r".*@(.*\.|)ac-lille\.fr$", - r".*@(.*\.|)ac-limoges\.fr$", - r".*@(.*\.|)ac-lyon\.fr$", - r".*@(.*\.|)ac-martinique\.fr$", - r".*@(.*\.|)ac-mayotte\.fr$", - r".*@(.*\.|)ac-montpellier\.fr$", - r".*@(.*\.|)ac-nancy-metz\.fr$", - r".*@(.*\.|)ac-nantes\.fr$", - r".*@(.*\.|)ac-nice\.fr$", - r".*@(.*\.|)ac-normandie\.fr$", - r".*@(.*\.|)ac-noumea\.nc$", - r".*@(.*\.|)ac-orleans-tours\.fr$", - r".*@(.*\.|)ac-paris\.fr$", - r".*@(.*\.|)ac-poitiers\.fr$", - r".*@(.*\.|)ac-polynesie\.pf$", - r".*@(.*\.|)ac-reims\.fr$", - r".*@(.*\.|)ac-rennes\.fr$", - r".*@(.*\.|)ac-reunion\.fr$", - r".*@(.*\.|)ac-rouen\.fr$", - r".*@(.*\.|)ac-spm\.fr$", - r".*@(.*\.|)ac-strasbourg\.fr$", - r".*@(.*\.|)ac-toulouse\.fr$", - r".*@(.*\.|)ac-versailles\.fr$", - r".*@(.*\.|)ac-wf\.wf$", - r".*@(.*\.|)acnusa\.fr$", - r".*@(.*\.|)acte-etat-civil\.fr$", - r".*@(.*\.|)ademe\.fr$", - r".*@(.*\.|)aefe\.fr$", - r".*@(.*\.|)afd\.fr$", - r".*@(.*\.|)agencebio\.org$", - r".*@(.*\.|)agence-regionale-sante\.fr$", - r".*@(.*\.|)anfr\.fr$", - r".*@(.*\.|)anses\.fr$", - r".*@(.*\.|)ansm\.sante\.fr$", - r".*@(.*\.|)aphp\.fr$", - r".*@(.*\.|)apij-justice\.fr$", - r".*@(.*\.|)arcep\.fr$", - r".*@(.*\.|)ars\.sante\.fr$", - r".*@(.*\.|)asi-aeroports\.fr$", - r".*@(.*\.|)asn\.fr$", - r".*@(.*\.|)asp-public\.fr$", - r".*@(.*\.|)assemblee-afe\.fr$", - r".*@(.*\.|)assurance-maladie\.fr$", - r".*@(.*\.|)attachefiscal\.org$", - r".*@(.*\.|)autorite-statistique-publique\.fr$", - r".*@(.*\.|)autoritedelaconcurrence\.fr$", - r".*@(.*\.|)bea\.aero$", - r".*@(.*\.|)biomedecine\.fr$", - r".*@(.*\.|)bnf\.fr$", - r".*@bnu\.fr$", - r".*@(.*\.|)businessfrance\.fr$", - r".*@(.*\.|)cabinet\.education\.fr$", - r".*@(.*\.|)cades\.fr$", - r".*@(.*\.|)ccomptes\.fr$", - r".*@(.*\.|)ccsp\.fr$", - r".*@cea\.fr$", - r".*@(.*\.|)cerema\.fr$", - r".*@(.*\.|)ch-bagneres\.fr$", - r".*@(.*\.|)ch-fidesien\.fr$", - r".*@(.*\.|)ch-lannemezan\.fr$", - r".*@(.*\.|)ch-lourdes\.fr$", - r".*@(.*\.|)ch-tarbes-vic\.fr$", - r".*@(.*\.|)chateauversailles\.fr$", - r".*@(.*\.|)chr-metz-thionville\.fr$", - r".*@chu-amiens\.fr$", - r".*@chu-montpellier\.fr$", - r".*@(.*\.|)chu-nimes\.fr$", - r".*@(.*\.|)cirad\.fr$", - r".*@(.*\.|)cnccep\.fr$", - r".*@(.*\.|)cncdh\.fr$", - r".*@(.*\.|)cnctr\.fr$", - r".*@(.*\.|)cndaspe\.fr$", - r".*@cne2\.fr$", - r".*@(.*\.|)cnil\.fr$", - r".*@(.*\.|)cnis\.fr$", - r".*@(.*\.|)cnpf\.fr$", - r".*@(.*\.|)cnr-elysee\.fr$", - r".*@(.*\.|)comite-du-label\.fr$", - r".*@(.*\.|)comite-du-secret\.fr$", - r".*@(.*\.|)commission-refugies\.fr$", - r".*@(.*\.|)conseil-concurrence\.fr$", - r".*@(.*\.|)conseil-etat\.fr$", - r".*@(.*\.|)cor-retraites\.fr$", - r".*@cre\.fr$", - r".*@crenau\.archi\.fr$", - r".*@(.*\.|)csa\.fr$", - r".*@(.*\.|)csnp\.fr$", - r".*@(.*\.|)culture\.fr$", - r".*@(.*\.|)debatpublic\.fr$", - r".*@(.*\.|)defenseurdesdroits\.fr$", - r".*@(.*\.|)dialogue-trianon\.fr$", - r".*@(.*\.|)ecoledulouvre\.fr$", - r".*@(.*\.|)efs\.sante\.fr$", - r".*@(.*\.|)elysee\.fr$", - r".*@(.*\.|)enac\.fr$", - r".*@(.*\.|)enim\.eu$", - r".*@ensai\.fr$", - r".*@(.*\.|)enssib\.fr$", - r".*@(.*\.|)ensta-paristech\.fr$", - r".*@(.*\.|)epaf\.asso\.fr$", - r".*@(.*\.|)epms-le-littoral\.net$", - r".*@(.*\.|)epms-le-littoral\.org$", - r".*@(.*\.|)erafp\.fr$", - r".*@(.*\.|)espace.gouv\.fr$", - r".*@(.*\.|)europol\.europa\.eu$", - r".*@(.*\.|)europolhq\.net$", - r".*@(.*\.|)fete-gastronomie\.fr$", - r".*@(.*\.|)fnap-logement\.fr$", - r".*@(.*\.|)fr\.europol\.net$", - r".*@(.*\.|)franceagrimer\.fr$", - r".*@(.*\.|)francemobilites\.fr$", - r".*@(.*\.|)frenchmobility\.fr$", - r".*@fun-mooc\.fr$", - r".*@(.*\.|)gouv\.fr$", - r".*@(.*\.|)guimet\.fr$", - r".*@(.*\.|)granddebat\.fr$", - r".*@(.*\.|)has-sante\.fr$", - r".*@(.*\.|)hautconseilclimat\.fr$", - r".*@(.*\.|)hautconseildesbiotechnologies\.fr$", - r".*@(.*\.|)hceres\.fr$", - r".*@(.*\.|)hcf-famille\.fr$", - r".*@(.*\.|)hcfp\.fr$", - r".*@(.*\.|)hebergement2\.interieur-gouv\.fr$", - r".*@(.*\.|)hebergement\.interieur-gouv\.fr$", - r".*@(.*\.|)hopital-le-montaigu\.com$", - r".*@(.*\.|)i-carre\.net$", - r".*@ibcp\.fr$", - r".*@(.*\.|)idda13\.fr$", - r".*@(.*\.|)ifce\.fr$", - r".*@(.*\.|)ign\.fr$", - r".*@ihedn\.fr$", - r".*@(.*\.|)ihest\.fr$", - r".*@(.*\.|)inha\.fr$", - r".*@(.*\.|)inhesj\.fr$", - r".*@(.*\.|)injep\.fr$", - r".*@(.*\.|)inp\.fr$", - r".*@(.*\.|)inpi\.fr$", - r".*@(.*\.|)inra\.fr$", - r".*@(.*\.|)inrae\.fr$", - r".*@(.*\.|)inrap\.fr$", - r".*@(.*\.|)inria\.fr$", - r".*@(.*\.|)insee\.fr$", - r".*@(.*\.|)insep\.fr$", - r".*@(.*\.|)institutcancer\.fr$", - r".*@(.*\.|)ints\.fr$", - r".*@iralille\.fr$", - r".*@ira-lille\.fr$", - r".*@(.*\.|)irisa\.fr$", - r".*@(.*\.|)irstea\.fr$", - r".*@(.*\.|)juradm\.fr$", - r".*@(.*\.|)justice\.fr$", - r".*@(.*\.|)ladocumentationfrancaise\.fr$", - r".*@(.*\.|)loria\.fr$", - r".*@(.*\.|)louvre\.fr$", - r".*@(.*\.|)medecine-de-proximite\.fr$", - r".*@(.*\.|)meteofrance\.fr$", - r".*@(.*\.|)mrccfr\.eu$", - r".*@(.*\.|)mrscfr\.eu$", - r".*@(.*\.|)mucem\.org$", - r".*@(.*\.|)musee-orangerie\.fr$", - r".*@(.*\.|)musee-orsay\.fr$", - r".*@(.*\.|)museepicassoparis.fr$", - r".*@(.*\.|)musee-rodin\.fr$", - r".*@(.*\.|)nancy\.archi\.fr$", - r".*@nantes\.archi$", - r".*@nantes\.archi\.fr$", - r".*@(.*\.|)notification\.service-public\.fr$", - r".*@(.*\.|)odeadom\.fr$", - r".*@(.*\.|)ofgl\.fr$", - r".*@(.*\.|)ofii\.fr$", - r".*@(.*\.|)onf\.fr$", - r".*@(.*\.|)oniam\.fr$", - r".*@parcoursup\.fr$", - r".*@(.*\.|)pibude\.com$", - r".*@(.*\.|)point-info-famille\.fr$", - r".*@(.*\.|)pointinfofamille\.fr$", - r".*@(.*\.|)region-academique-aura\.fr$", - r".*@(.*\.|)region-academique-auvergne-rhone-alpes\.fr$", - r".*@(.*\.|)region-academique-bfc\.fr$", - r".*@(.*\.|)region-academique-bourgogne-franche-comte\.fr$", - r".*@(.*\.|)region-academique-bretagne\.fr$", - r".*@(.*\.|)region-academique-centre-val-de-loire\.fr$", - r".*@(.*\.|)region-academique-corse\.fr$", - r".*@(.*\.|)region-academique-grand-est\.fr$", - r".*@(.*\.|)region-academique-guadeloupe\.fr$", - r".*@(.*\.|)region-academique-guyane\.fr$", - r".*@(.*\.|)region-academique-hauts-de-france\.fr$", - r".*@(.*\.|)region-academique-idf\.fr$", - r".*@(.*\.|)region-academique-ile-de-france\.fr$", - r".*@(.*\.|)region-academique-martinique\.fr$", - r".*@(.*\.|)region-academique-mayotte\.fr$", - r".*@(.*\.|)region-academique-normandie\.fr$", - r".*@(.*\.|)region-academique-nouvelle-aquitaine\.fr$", - r".*@(.*\.|)region-academique-occitanie\.fr$", - r".*@(.*\.|)region-academique-paca\.fr$", - r".*@(.*\.|)region-academique-pays-de-la-loire\.fr$", - r".*@(.*\.|)region-academique-provence-alpes-cote-dazur\.fr$", - r".*@(.*\.|)region-academique-reunion\.fr$", - r".*@(.*\.|)regis-dgac\.net$", - r".*@renater\.fr$", - r".*@(.*\.|)santepubliquefrance\.fr$", - r".*@(.*\.|)service-eco\.fr$", - r".*@(.*\.|)service-public\.fr$", - r".*@(.*\.|)service-public\.fr\.preprod\.ext\.dila\.fr$", - r".*@(.*\.|)service-public\.fr\.qualif\.ext\.dila\.fr$", - r".*@(.*\.|)sevresciteceramique\.fr$", - r".*@(.*\.|)shom\.fr$", - r".*@societedugrandparis\.fr$", - r".*@(.*\.|)taaf\.fr$", - r".*@(.*\.|)telerecours\.fr$", - r".*@(.*\.|)theatre-odeon\.fr$", - r".*@(.*\.|)ugap\.fr$", - r".*@(.*\.|)unedic\.fr$", - r".*@univ-orleans\.fr$", - r".*@(.*\.|)univ-paris13\.fr$", - r".*@univ-perp\.fr$", - r".*@(.*\.|)vie-publique\.fr$", - r".*@(.*\.|)vnf\.fr$", - r".*@univ-ubs\.fr$", - r".*@sdis(?!00|20|69|75|96|97|98|99)[0-9]{2}.fr$", - r".*@sdis-vendee.fr$", - r".*@sdis21.org$", - r".*@sdis36.org$", - r".*@sdis67.com$", - r".*@sdis86.net$", - r".*@sdis97[1-3].fr$", - r".*@sdis976.fr$", - r".*@sdis974.re$", - r".*@(.*\.|)intranet-sdis11\.fr$", - r".*@pompiersparis\.fr$", - r".*@chu-angers\.fr$", - r".*@ensc-rennes\.fr$", - r".*@educagri\.fr$", - r".*@fiva\.fr$", - r".*@assurance-maladie\.fr$", - r".*@crous-aix-marseille\.fr$", - r".*@crous-amiens\.fr$", - r".*@crous-antillesguyane\.fr$", - r".*@crous-bordeaux\.fr$", - r".*@crous-bfc\.fr$", - r".*@crous-clermont\.fr$", - r".*@crous-corse\.fr$", - r".*@crous-creteil\.fr$", - r".*@crous-grenoble\.fr$", - r".*@crous-reunion\.fr$", - r".*@crous-lille\.fr$", - r".*@crous-limoges\.fr$", - r".*@crous-lorraine\.fr$", - r".*@crous-lyon\.fr$", - r".*@crous-montpellier\.fr$", - r".*@crous-nantes\.fr$", - r".*@crous-nice\.fr$", - r".*@crous-normandie\.fr$", - r".*@crous-orleans-tours\.fr$", - r".*@crous-paris\.fr$", - r".*@crous-poitiers\.fr$", - r".*@crous-reims\.fr$", - r".*@crous-strasbourg\.fr$", - r".*@crous-versailles\.fr$", - r".*@crous-rennes\.fr$", - r".*@crous-toulouse\.fr$", - r".*@(.*\.|)assemblee-nationale\.fr$", - r".*@(.*\.|)senat\.fr$", - r".*@ird.fr$", - r".*@ch-chatillon.fr$", - r".*@ch-buzancais.fr$", -] +EMAIL_WHITELIST = DEFAULT_EMAIL_WHITELIST From 9554ddd965ce8c517f853d9b67694dfe4723ebb6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89loi=20Rivard?= Date: Wed, 6 Dec 2023 18:10:04 +0100 Subject: [PATCH 2/4] feat: parse configuration with pydantic_settings --- poetry.lock | 2 +- pyproject.toml | 1 + web.env.example | 6 +- web/flaskr/__init__.py | 18 +- web/flaskr/settings.py | 1043 ++++++++++++++++++++++++++++++++++++++++ 5 files changed, 1060 insertions(+), 10 deletions(-) create mode 100644 web/flaskr/settings.py diff --git a/poetry.lock b/poetry.lock index 8842eb16..577f2261 100644 --- a/poetry.lock +++ b/poetry.lock @@ -2629,4 +2629,4 @@ testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "p [metadata] lock-version = "2.0" python-versions = ">=3.8.1,<4.0" -content-hash = "e9a01df95807b879faef0e8113445565b27bff054d8d289fad18e56c08536646" +content-hash = "821efb9756c168ad7eec23c00b864275d14affa92cae2f940ca0667ad55e9b4e" diff --git a/pyproject.toml b/pyproject.toml index cb270dcd..fba412e9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,6 +31,7 @@ flask-wtf = "^1.1.1" redis = "4.4.4" requests = "^2.27.1" werkzeug = "<2.3" +pydantic-settings = "^2.1.0" [tool.poetry.group.dev] optional = true diff --git a/web.env.example b/web.env.example index fb61a312..b4689879 100644 --- a/web.env.example +++ b/web.env.example @@ -43,7 +43,7 @@ OIDC_USERINFO_HTTP_METHOD=POST OIDC_REDIRECT_URI=http://localhost:5000/oidc_callback # Attendee OIDC Configuration (back to default if empty) -OIDC_ATTENDEE_ENABLED= +OIDC_ATTENDEE_ENABLED=true OIDC_ATTENDEE_ISSUER= OIDC_ATTENDEE_CLIENT_ID= OIDC_ATTENDEE_CLIENT_SECRET= @@ -62,9 +62,9 @@ SMTP_HOST=0.0.0.0 SMTP_PORT=1025 SMTP_USERNAME= SMTP_PASSWORD= -SMTP_SSL= +SMTP_SSL=false EMAIL_WHITELIST=.*@(.*\.|)gouv\.fr -RIE_NETWORK_IPS=192.168.0.0/16,172.16.0.0/12,192.168.1.97/16,127.0.0.0/16 +RIE_NETWORK_IPS=192.168.0.0/16,172.16.0.0/12,127.0.0.0/16 BIGBLUEBUTTON_ANALYTICS_CALLBACK_URL=https://bbb-analytics-staging.osc-fr1.scalingo.io/v1/post_events # Nextcloud and meeting Files connection configuration diff --git a/web/flaskr/__init__.py b/web/flaskr/__init__.py index 23bcca8c..64614b02 100755 --- a/web/flaskr/__init__.py +++ b/web/flaskr/__init__.py @@ -18,6 +18,7 @@ from flask_caching import Cache from flask_migrate import Migrate from flask_wtf.csrf import CSRFProtect +from flaskr.settings import MainSettings from flaskr.utils import is_rie @@ -27,6 +28,14 @@ cache = Cache() +def setup_configuration(app, config=None): + app.config.from_pyfile("config.py") + if config: + app.config.from_mapping(config) + + MainSettings.model_validate(config) + + def setup_cache(app): cache.init_app( app, @@ -37,14 +46,11 @@ def setup_cache(app): ) -def setup_logging(app, test_config=None, gunicorn_logging=False): +def setup_logging(app, gunicorn_logging=False): if gunicorn_logging: gunicorn_logger = logging.getLogger("gunicorn.error") app.logger.handlers = gunicorn_logger.handlers app.logger.setLevel(gunicorn_logger.level) - app.config.from_pyfile("config.py") - if test_config: - app.config.from_mapping(test_config) def setup_i18n(app): @@ -108,10 +114,10 @@ def internal_error(error): def create_app(test_config=None, gunicorn_logging=False): - # create and configure the app app = Flask(__name__, instance_relative_config=True) + setup_configuration(app, test_config) setup_cache(app) - setup_logging(app, test_config, gunicorn_logging) + setup_logging(app, gunicorn_logging) setup_i18n(app) setup_csrf(app) setup_database(app) diff --git a/web/flaskr/settings.py b/web/flaskr/settings.py new file mode 100644 index 00000000..8a8bc113 --- /dev/null +++ b/web/flaskr/settings.py @@ -0,0 +1,1043 @@ +import json +from typing import Any +from typing import Dict +from typing import List +from typing import Optional + +from flask_babel import lazy_gettext as _ +from flaskr.constants import DEFAULT_EMAIL_WHITELIST +from pydantic import computed_field +from pydantic import field_validator +from pydantic import FieldValidationInfo +from pydantic_settings import BaseSettings +from pydantic_settings import SettingsConfigDict + +AVAILABLE_WORDINGS = { + "MEETING": {"cours": "cours", "reunion": "réunion", "seminaire": "séminaire"}, + "MEETINGS": {"cours": "cours", "reunion": "réunions", "seminaire": "séminaires"}, + "A_MEETING": { + "cours": "un cours", + "reunion": "une réunion", + "seminaire": "un séminaire", + }, + "MY_MEETING": { + "cours": "mon cours", + "reunion": "ma réunion", + "seminaire": "mon séminaire", + }, + "THE_MEETING": { + "cours": "le cours", + "reunion": "la réunion", + "seminaire": "le séminaire", + }, + "OF_THE_MEETING": { + "cours": "du cours", + "reunion": "de la réunion", + "seminaire": "du séminaire", + }, + "THIS_MEETING": { + "cours": "ce cours", + "reunion": "cette réunion", + "seminaire": "ce séminaire", + }, + "TO_THE_MEETING": { + "cours": "au cours", + "reunion": "à la réunion", + "seminaire": "au séminaire", + }, + "IMPROVISED_MEETING": { + "cours": "cours improvisé", + "reunion": "réunion improvisée", + "seminaire": "séminaire improvisé", + }, + "AN_IMPROVISED_MEETING": { + "cours": "un cours improvisé", + "reunion": _("une réunion improvisée"), + "seminaire": "un séminaire improvisé", + }, + "A_QUICK_MEETING": { + "cours": "un cours immédiat", + "reunion": _("une réunion immédiate"), + "seminaire": "un séminaire immédiat", + }, + "PRIVATE_MEETINGS": { + "cours": "cours privés", + "reunion": _("réunions privées"), + "seminaire": "séminaires privés", + }, + "GOOD_MEETING": { + "cours": "bon cours", + "reunion": _("bonne réunion"), + "seminaire": "bon séminaire", + }, + "MEETING_UNDEFINED_ARTICLE": { + "cours": "un", + "reunion": _("une"), + "seminaire": "un", + }, + "A_MEETING_TO_WHICH": { + "cours": "un cours auquel", + "reunion": _("une réunion à laquelle"), + "seminaire": "un séminaire auquel", + }, + "WELCOME_PAGE_SUBTITLE": { + "cours": _( + "Créez un cours immédiatement avec des réglages standards. Ce cours ne sera pas enregistré dans votre liste de salons." + ), + "reunion": _( + "Créez une réunion immédiatement avec des réglages standards. Cette réunion ne sera pas enregistrée dans votre liste de salons." + ), + "seminaire": _( + "Créez un séminaire immédiatement avec des réglages standards. Ce séminaire ne sera pas enregistré dans votre liste de salons." + ), + }, + "MEETING_MAIL_SUBJECT": { + "cours": _("Invitation à un cours en ligne immédiat du Webinaire de l’Etat"), + "reunion": _( + "Invitation à une réunion en ligne immédiat du Webinaire de l’Etat" + ), + "seminaire": _( + "Invitation à un séminaire en ligne immédiat du Webinaire de l’Etat" + ), + }, +} + + +class MainSettings(BaseSettings): + """ + Paramètres de configuration du frontal B3Desk. + """ + + model_config = SettingsConfigDict(extra="allow") + + SECRET_KEY: str + """ + Clé secrète utilisée notamment pour la signature des cookies. + Cette clé DOIT être différente pour TOUTES les instances de B3Desk, et être tenue secrète. + Peut être générée avec ``python -c 'import secrets; print(secrets.token_hex())'`` + + Plus d'infos sur https://flask.palletsprojects.com/en/3.0.x/config/#SECRET_KEY + """ + + REDIS_URL: Optional[str] = None + """ + L’URL du serveur redis utilisé pour les tâches asynchrones. + Par exemple ``localhost:6379``. + """ + + NC_LOGIN_TIMEDELTA_DAYS: int = 30 + """ + Durée en jours avant l’expiration des autorisations Nextcloud. + """ + + NC_LOGIN_API_URL: Optional[str] = None + """ + Point d’accès du fournisseur Nextcloud. + Par exemple ``https://cloud.example.org/index.php``. + """ + + NC_LOGIN_API_KEY: Optional[str] = None + """ + Clé d'API Nextcloud. + """ + + UPLOAD_DIR: str + """ + Chemin vers le dossier dans lequel seront stockés les fichiers téléversés par les utilisateurs. + """ + + TMP_DOWNLOAD_DIR: str + """ + Chemin vers un dossier qui servira de stockage temporaire de fichiers entre Nextcloud et BBB. + """ + + MAX_SIZE_UPLOAD: int = 20000000 + """ + Taille maximum des fichiers téléversés, en octets. + """ + + TIME_FORMAT: str = "%Y-%m-%d" + """ + Format des dates utilisées lors des échanges avec l’API de Nextcloud. + + Plus d’informations sur https://docs.python.org/fr/3/library/datetime.html#strftime-and-strptime-format-codes + """ + + TESTING: bool = False + """ + Mode tests unitaires, à ne surtout pas utiliser en production. + + Plus d’informations sur https://flask.palletsprojects.com/en/3.0.x/config/#TESTING + """ + + DEBUG: bool = False + """ + Mode debug, à ne surtout pas utiliser en production. + + Plus d’informations sur https://flask.palletsprojects.com/en/3.0.x/config/#DEBUG + """ + + TITLE: str = "BBB-Visio" + """ + Titre HTML par défaut pour les pages HTML. + """ + + # TODO: Replace this with SERVER_NAME + url_for(..., _external=True) + SERVER_FQDN: str + """ + L’adresse publique du serveur B3Desk. + Par exemple ``https://b3desk.example.org``. + """ + + EXTERNAL_UPLOAD_DESCRIPTION: str = "Fichiers depuis votre Nextcloud" + """ + Description dans BBB des fichiers téléversés dans Nextcloud. + """ + + WTF_CSRF_TIME_LIMIT: int = 3600 * 12 + """ + Indique en secondes la durée de validit des jetons CSRF. + + Plus d'infos sur https://flask-wtf.readthedocs.io/en/1.2.x/config/ + """ + + BABEL_TRANSLATION_DIRECTORIES: str = "/opt/bbb-visio/translations" + """ + Un ou plusieurs chemins vers les répertoires des catalogues de traduction, séparés par des « ; ». + + Plus d’infos sur https://python-babel.github.io/flask-babel/#configuration + """ + + MAX_MEETINGS_PER_USER: int = 50 + """ + Le nombre maximum de séminaires que peut créer un utilisateur. + """ + + ALLOWED_MIME_TYPES_SERVER_SIDE: Optional[List[str]] = [ + "application/pdf", + "image/vnd.dwg", + "image/x-xcf", + "image/jpeg", + "image/jpx", + "image/png", + "image/apng", + "image/gif", + "image/webp", + "image/x-canon-cr2", + "image/tiff", + "image/bmp", + "image/vnd.ms-photo", + "image/vnd.adobe.photoshop", + "image/x-icon", + "image/heic", + "image/avif", + "application/msword", + "application/vnd.openxmlformats-officedocument.wordprocessingml.document", + "application/vnd.oasis.opendocument.text", + "application/vnd.ms-excel", + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + "application/vnd.oasis.opendocument.spreadsheet", + "application/vnd.ms-powerpoint", + "application/vnd.openxmlformats-officedocument.presentationml.presentation", + "application/vnd.oasis.opendocument.presentation", + ] + """ + Liste de mime-types acceptés par le serveur pour le téléversement de fichiers. + Si non renseigné, tous les fichiers sont autorisés. + """ + + @field_validator("ALLOWED_MIME_TYPES_SERVER_SIDE", mode="before") + def get_allowed_mime_types_server_side( + cls, + allowed_mime_types_server_side: Optional[List[str]], + info: FieldValidationInfo, + ) -> List[str]: + if not allowed_mime_types_server_side: + return [] + + if isinstance(allowed_mime_types_server_side, str): + return json.loads(allowed_mime_types_server_side) + + return allowed_mime_types_server_side + + ACCEPTED_FILES_CLIENT_SIDE: Optional[ + str + ] = "image/*,.pdf,.doc,.docx,.htm,.html,.odp,.ods,.odt,.ppt,.pptx,.xls,.xlsx" + """ + Liste de mime-types autorisés par le navigateur pour le téléversement des fichiers, séparés par des virgules. + + Passé en paramètre ``acceptedFiles`` de *Dropzone*. + + Plus d’infos sur https://docs.dropzone.dev/configuration/basics/configuration-options + """ + + OIDC_ID_TOKEN_COOKIE_SECURE: bool = False + """ + Probablement un relicat de flask-oidc, semble inutilisé. + """ + + OIDC_REQUIRE_VERIFIED_EMAIL: bool = False + """ + Probablement un relicat de flask-oidc, semble inutilisé. + """ + + OIDC_USER_INFO_ENABLED: bool = True + """ + Probablement un relicat de flask-oidc, semble inutilisé. + """ + + OIDC_OPENID_REALM: str = "apps" + """ + Probablement un relicat de flask-oidc, semble inutilisé. + """ + + OIDC_SCOPES: List[str] = ["openid", "email", "profile"] + """ + Liste des scopes OpenID Connect pour lesquels une autorisation sera demandée au serveur d’identité, séparés par des virgules. + + Passé en paramètre ``auth_request_params`` de flask-pyoidc. + Plus d’infos sur https://flask-pyoidc.readthedocs.io/en/latest/api.html#module-flask_pyoidc.provider_configuration + """ + + @field_validator("OIDC_SCOPES", mode="before") + def get_oidc_scopes(cls, oidc_scopes: List[str], info: FieldValidationInfo) -> str: + return oidc_scopes.split(",") if isinstance(oidc_scopes, str) else oidc_scopes + + OIDC_INTROSPECTION_AUTH_METHOD: str = "client_secret_post" + """ + Probablement un relicat de flask-oidc, semble inutilisé. + """ + + OIDC_USERINFO_HTTP_METHOD: str = "POST" + """ + Méthode ``GET`` ou ``POST`` à utiliser pour les requêtes sur le point d’entrée *UserInfo* du serveur d’identité. + + Plus d’infos sur https://flask-pyoidc.readthedocs.io/en/latest/api.html?highlight=userinfo_http_method#flask_pyoidc.provider_configuration.ProviderConfiguration + """ + + OIDC_INFO_REQUESTED_FIELDS: List[str] = ["email", "given_name", "family_name"] + """ + Probablement un relicat de flask-oidc, semble inutilisé. + """ + + OIDC_ISSUER: Optional[str] = None + """ + URL du serveur d’identité des organisateurs de réunion. + + Par exemple : https://auth.example.com + """ + + OIDC_AUTH_URI: Optional[str] = None + """ + Probablement un relicat de flask-oidc, semble inutilisé. + """ + + OIDC_USERINFO_URI: Optional[str] = None + """ + Probablement un relicat de flask-oidc, semble inutilisé. + """ + + OIDC_TOKEN_URI: Optional[str] = None + """ + Probablement un relicat de flask-oidc, semble inutilisé. + """ + + OIDC_CLIENT_ID: Optional[str] = None + """ + ID du client auprès du serveur d’identité des organisateurs. + """ + + OIDC_CLIENT_SECRET: Optional[str] = None + """ + Secret permettant d’identifier le client auprès du serveur d’identité des organisateurs. + """ + + OIDC_CLIENT_AUTH_METHOD: Optional[str] = "client_secret_post" + """ + Méthode de communication avec le point d’entrée ``token_introspection_uri`` du serveur d’identité des organisateurs. Surcharge probablement ``OIDC_INTROSPECTION_AUTH_METHOD``. + """ + + # TODO: replace by OIDCAuthentication.redirect_uri_config + OIDC_REDIRECT_URI: Optional[str] = None + """ + URL de B3Desk vers laquelle le serveur d’identité redirige + les utilisateurs après authentification. + + Par exemple ``http://localhost:5000/oidc_callback`` + + Plus d’infos sur https://flask-pyoidc.readthedocs.io/en/latest/configuration.html?highlight=OIDC_REDIRECT_URI#static-client-registration + """ + + OIDC_SERVICE_NAME: Optional[str] = None + """ + Probablement un relicat de flask-oidc, semble inutilisé à part en valeur par défaut de ``OIDC_ATTENDEE_SERVICE_NAME``. + """ + + # Attendee OIDC Configuration (back to default if empty) + OIDC_ATTENDEE_ENABLED: Optional[bool] = True + """ + Indique si le serveur d’authentification des participants est activé ou non. + Si le serveur est KO, en passant cette variable à ``False``, + l’authentification ne sera plus nécessaire pour les liens d’invitation authentifiés, ce qui permet de faire en sorte que les liens restent valides. + """ + + OIDC_ATTENDEE_ISSUER: Optional[str] = None + """ + URL du serveur d’identité des participants authentifiés. + + Si non renseigné, prend la valeur de ``OIDC_ISSUER``. + """ + + OIDC_ATTENDEE_CLIENT_ID: Optional[str] = None + """ + ID du client auprès du serveur d’identité des participants authentifiés. + + Si non renseigné, prend la valeur de ``OIDC_CLIENT_ID``. + """ + + OIDC_ATTENDEE_CLIENT_SECRET: Optional[str] = None + """ + Secret permettant d’identifier le client auprès du serveur d’identité des participants authentifiés. + + Si non renseigné, prend la valeur de ``OIDC_CLIENT_ID``. + """ + + OIDC_ATTENDEE_CLIENT_AUTH_METHOD: Optional[str] = None + """ + Méthode de communication avec le point d’entrée ``token_introspection_uri`` du serveur d’identité des participants authentifiés. Surcharge probablement ``OIDC_INTROSPECTION_AUTH_METHOD``. + + Si non renseigné, prend la valeur de ``OIDC_CLIENT_AUTH_METHOD``. + """ + + OIDC_ATTENDEE_USERINFO_HTTP_METHOD: Optional[str] = None + """ + Méthode ``GET`` ou ``POST`` à utiliser pour les requêtes sur le point d’entrée *UserInfo* du serveur d’identité. + + Si non renseigné, prend la valeur de ``OIDC_USERINFO_HTTP_METHOD``. + + Plus d’infos sur https://flask-pyoidc.readthedocs.io/en/latest/api.html?highlight=userinfo_http_method#flask_pyoidc.provider_configuration.ProviderConfiguration + """ + + OIDC_ATTENDEE_SERVICE_NAME: Optional[str] = None + """ + Nom du service d’authentification des participants authentifiés. + Utilisé pour l’affichage dans la modale d’invitation de participants authentifés. + + Si non renseigné, prend la valeur de ``OIDC_SERVICE_NAME``. + """ + + OIDC_ATTENDEE_SCOPES: Optional[List[str]] = None + """ + Liste des scopes OpenID Connect pour lesquels une autorisation sera demandée au serveur d’identité des participants authentifiés, séparés par des virgules. + + Si non renseigné, prend la valeur de ``OIDC_SCOPES``. + + Passé en paramètre ``auth_request_params`` de flask-pyoidc. + Plus d’infos sur https://flask-pyoidc.readthedocs.io/en/latest/api.html#module-flask_pyoidc.provider_configuration + """ + + @field_validator("OIDC_ATTENDEE_ISSUER") + def get_attendee_issuer( + cls, attendee_issuer: str, info: FieldValidationInfo + ) -> str: + return attendee_issuer or info.data.get("OIDC_ISSUER") + + @field_validator("OIDC_ATTENDEE_CLIENT_ID") + def get_attendee_client_id( + cls, attendee_client_id: str, info: FieldValidationInfo + ) -> str: + return attendee_client_id or info.data.get("OIDC_CLIENT_ID") + + @field_validator("OIDC_ATTENDEE_CLIENT_SECRET") + def get_attendee_client_secret( + cls, attendee_client_secret: str, info: FieldValidationInfo + ) -> str: + return attendee_client_secret or info.data.get("OIDC_CLIENT_SECRET") + + @field_validator("OIDC_ATTENDEE_CLIENT_AUTH_METHOD") + def get_attendee_client_auth_method( + cls, attendee_client_auth_method: str, info: FieldValidationInfo + ) -> str: + return attendee_client_auth_method or info.data.get("OIDC_CLIENT_AUTH_METHOD") + + @field_validator("OIDC_ATTENDEE_USERINFO_HTTP_METHOD") + def get_attendee_userinfo_http_method( + cls, attendee_userinfo_http_method: str, info: FieldValidationInfo + ) -> str: + return attendee_userinfo_http_method or info.data.get( + "OIDC_USERINFO_HTTP_METHOD" + ) + + @field_validator("OIDC_ATTENDEE_SERVICE_NAME") + def get_attendee_attendee_service_name( + cls, attendee_attendee_service_name: str, info: FieldValidationInfo + ) -> str: + return attendee_attendee_service_name or info.data.get("OIDC_SERVICE_NAME") + + @field_validator("OIDC_ATTENDEE_SCOPES") + def get_attendee_attendee_scopes( + cls, attendee_scopes: str, info: FieldValidationInfo + ) -> str: + scopes = attendee_scopes or info.data.get("OIDC_SCOPES") + return scopes.split(",") if isinstance(scopes, str) else scopes + + DOCUMENTATION_LINK_URL: Optional[str] = None + """ + Surcharge l’adresse de la page de documentation si renseigné. + """ + + DOCUMENTATION_LINK_LABEL: Optional[str] = None + """ + Semble inutilisé. + """ + + @computed_field() + def DOCUMENTATION_LINK(self) -> Dict[str, Any]: + return { + "url": self.DOCUMENTATION_LINK_URL, + "label": self.DOCUMENTATION_LINK_LABEL, + "is_external": self.DOCUMENTATION_LINK_URL.lower().startswith( + ("/", self.SERVER_FQDN.lower()) + ), + } + + SERVICE_TITLE: str = "Webinaire" + """ + Nom du service B3Desk. + """ + + SERVICE_TAGLINE: str = "Le service de webinaire pour les agents de l’État" + """ + Slogan du service B3Desk. + """ + + MEETING_LOGOUT_URL: Optional[str] = None + """ + URL vers laquelle sont redirigés les utilisateurs après un séminaire. + """ + + SATISFACTION_POLL_URL: Optional[str] = None + """ + URL de l’iframe du formulaire de satisfaction. + """ + + SQLALCHEMY_DATABASE_URI: str + """ + URI de configuration de la base de données. + + Par exemple ``postgresql://user:password@localhost:5432/bbb_visio`` + """ + + # TODO: delete as this is the default in flask-sqlalchemy 3? + SQLALCHEMY_TRACK_MODIFICATIONS: bool = False + """ + Traçage des évènements de modification des modèles dans SQLAlchemy. + + Plus d’informations sur https://flask-sqlalchemy.palletsprojects.com/en/3.1.x/track-modifications/ + """ + + MEETING_KEY_WORDING: str = "reunion" + """ + Nommage des réunions. Peut-être *reunion*, *cours* ou *séminaire*. + """ + + WORDING_A_MEETING: Any = None + """ + Formulation de « une réunion », par exemple *un cours* ou *un séminaire*. Par défaut s’adapte à ``MEETING_KEY_WORDING``. + """ + + @field_validator("WORDING_A_MEETING") + def get_wording_a_meeting( + cls, wording_a_meeting: Any, info: FieldValidationInfo + ) -> Any: + return ( + wording_a_meeting + or AVAILABLE_WORDINGS["A_MEETING"][info.data["MEETING_KEY_WORDING"]] + ) + + WORDING_MY_MEETING: Any = None + """ + Formulation de « ma réunion », par exemple *mon cours* ou *mon séminaire*. Par défaut s’adapte à ``MEETING_KEY_WORDING``. + """ + + @field_validator("WORDING_MY_MEETING") + def get_wording_my_meeting( + cls, wording_my_meeting: Any, info: FieldValidationInfo + ) -> Any: + return ( + wording_my_meeting + or AVAILABLE_WORDINGS["MY_MEETING"][info.data["MEETING_KEY_WORDING"]] + ) + + WORDING_THE_MEETING: Any = None + """ + Formulation de « la réunion », par exemple *le cours* ou *le séminaire*. Par défaut s’adapte à ``MEETING_KEY_WORDING``. + """ + + @field_validator("WORDING_THE_MEETING") + def get_wording_the_meeting( + cls, wording_the_meeting: Any, info: FieldValidationInfo + ) -> Any: + return ( + wording_the_meeting + or AVAILABLE_WORDINGS["THE_MEETING"][info.data["MEETING_KEY_WORDING"]] + ) + + WORDING_OF_THE_MEETING: Any = None + """ + Formulation de « de la réunion », par exemple *du cours* ou *du séminaire*. Par défaut s’adapte à ``MEETING_KEY_WORDING``. + """ + + @field_validator("WORDING_OF_THE_MEETING") + def get_wording_of_the_meeting( + cls, wording_of_the_meeting: Any, info: FieldValidationInfo + ) -> Any: + return ( + wording_of_the_meeting + or AVAILABLE_WORDINGS["OF_THE_MEETING"][info.data["MEETING_KEY_WORDING"]] + ) + + WORDING_MEETING: Any = None + """ + Formulation de « réunion », par exemple *cours* ou *séminaire*. Par défaut s’adapte à ``MEETING_KEY_WORDING``. + """ + + @field_validator("WORDING_MEETING") + def get_wording_meeting( + cls, wording_meeting: Any, info: FieldValidationInfo + ) -> Any: + return ( + wording_meeting + or AVAILABLE_WORDINGS["MEETING"][info.data["MEETING_KEY_WORDING"]] + ) + + WORDING_MEETINGS: Any = None + """ + Formulation de « réunions », par exemple *cours* ou *séminaires*. Par défaut s’adapte à ``MEETING_KEY_WORDING``. + """ + + @field_validator("WORDING_MEETINGS") + def get_wording_meetings( + cls, wording_meetings: Any, info: FieldValidationInfo + ) -> Any: + return ( + wording_meetings + or AVAILABLE_WORDINGS["MEETINGS"][info.data["MEETING_KEY_WORDING"]] + ) + + WORDING_THIS_MEETING: Any = None + """ + Formulation de « cette réunion », par exemple *ce cours* ou *ce séminaires*. Par défaut s’adapte à ``MEETING_KEY_WORDING``. + """ + + @field_validator("WORDING_THIS_MEETING") + def get_wording_this_meeting( + cls, wording_this_meeting: Any, info: FieldValidationInfo + ) -> Any: + return ( + wording_this_meeting + or AVAILABLE_WORDINGS["THIS_MEETING"][info.data["MEETING_KEY_WORDING"]] + ) + + WORDING_TO_THE_MEETING: Any = None + """ + Formulation de « à la réunion », par exemple *au cours* ou *au séminaires*. Par défaut s’adapte à ``MEETING_KEY_WORDING``. + """ + + @field_validator("WORDING_TO_THE_MEETING") + def get_wording_to_the_meeting( + cls, wording_to_the_meeting: Any, info: FieldValidationInfo + ) -> Any: + return ( + wording_to_the_meeting + or AVAILABLE_WORDINGS["TO_THE_MEETING"][info.data["MEETING_KEY_WORDING"]] + ) + + WORDING_IMPROVISED_MEETING: Any = None + """ + Formulation de « réunion improvisée », par exemple *cours improvisé* ou *séminaire improvisé*. Par défaut s’adapte à ``MEETING_KEY_WORDING``. + """ + + @field_validator("WORDING_IMPROVISED_MEETING") + def get_wording_improvised_meeting( + cls, wording_improvised_meeting: Any, info: FieldValidationInfo + ) -> Any: + return ( + wording_improvised_meeting + or AVAILABLE_WORDINGS["IMPROVISED_MEETING"][ + info.data["MEETING_KEY_WORDING"] + ] + ) + + WORDING_AN_IMPROVISED_MEETING: Any = None + """ + Formulation de « une réunion improvisée », par exemple *un cours improvisé* ou *un séminaire improvisé*. Par défaut s’adapte à ``MEETING_KEY_WORDING``. + """ + + @field_validator("WORDING_AN_IMPROVISED_MEETING") + def get_wording_an_improvised_meeting( + cls, wording_an_improvised_meeting: Any, info: FieldValidationInfo + ) -> Any: + return ( + wording_an_improvised_meeting + or AVAILABLE_WORDINGS["AN_IMPROVISED_MEETING"][ + info.data["MEETING_KEY_WORDING"] + ] + ) + + WORDING_A_QUICK_MEETING: Any = None + """ + Formulation de « une réunion immédiate », par exemple *un cours immédiat* ou *un séminaire immédiat*. Par défaut s’adapte à ``MEETING_KEY_WORDING``. + """ + + @field_validator("WORDING_A_QUICK_MEETING") + def get_wording_a_quick_meeting( + cls, wording_a_quick_meeting: Any, info: FieldValidationInfo + ) -> Any: + return ( + wording_a_quick_meeting + or AVAILABLE_WORDINGS["A_QUICK_MEETING"][info.data["MEETING_KEY_WORDING"]] + ) + + WORDING_PRIVATE_MEETINGS: Any = None + """ + Formulation de « réunions privées », par exemple *cours privés* ou *séminaires privés*. Par défaut s’adapte à ``MEETING_KEY_WORDING``. + """ + + @field_validator("WORDING_PRIVATE_MEETINGS") + def get_wording_private_meetings( + cls, wording_private_meetings: Any, info: FieldValidationInfo + ) -> Any: + return ( + wording_private_meetings + or AVAILABLE_WORDINGS["PRIVATE_MEETINGS"][info.data["MEETING_KEY_WORDING"]] + ) + + WORDING_GOOD_MEETING: Any = None + """ + Formulation de « bonne réunion », par exemple *bon cours* ou *bon séminaire*. Par défaut s’adapte à ``MEETING_KEY_WORDING``. + """ + + @field_validator("WORDING_GOOD_MEETING") + def get_wording_good_meeting( + cls, wording_good_meeting: Any, info: FieldValidationInfo + ) -> Any: + return ( + wording_good_meeting + or AVAILABLE_WORDINGS["GOOD_MEETING"][info.data["MEETING_KEY_WORDING"]] + ) + + WORDING_MEETING_UNDEFINED_ARTICLE: Any = None + """ + Formulation de l’article indéterminé de « réunion » comme « une », par exemple *un*. Par défaut s’adapte à ``MEETING_KEY_WORDING``. + """ + + @field_validator("WORDING_MEETING_UNDEFINED_ARTICLE") + def get_wording_meeting_undefined_article( + cls, wording_meeting_undefined_article: Any, info: FieldValidationInfo + ) -> Any: + return ( + wording_meeting_undefined_article + or AVAILABLE_WORDINGS["MEETING_UNDEFINED_ARTICLE"][ + info.data["MEETING_KEY_WORDING"] + ] + ) + + WORDING_A_MEETING_TO_WHICH: Any = None + """ + Formulation de « une réunion à laquelle », par exemple *un cours auquel* ou *un séminaire auquel*. Par défaut s’adapte à ``MEETING_KEY_WORDING``. + """ + + @field_validator("WORDING_A_MEETING_TO_WHICH") + def get_wording_a_meeting_to_which( + cls, wording_a_meeting_to_which: Any, info: FieldValidationInfo + ) -> Any: + return ( + wording_a_meeting_to_which + or AVAILABLE_WORDINGS["A_MEETING_TO_WHICH"][ + info.data["MEETING_KEY_WORDING"] + ] + ) + + WELCOME_PAGE_SUBTITLE: Any = None + """ + Formulation du sous-titre de la page de création de réunion. Par défaut s’adapte à ``MEETING_KEY_WORDING``. + """ + + @field_validator("WELCOME_PAGE_SUBTITLE") + def get_welcome_page_subtitle( + cls, welcome_page_subtitle: Any, info: FieldValidationInfo + ) -> Any: + return ( + welcome_page_subtitle + or AVAILABLE_WORDINGS["WELCOME_PAGE_SUBTITLE"][ + info.data["MEETING_KEY_WORDING"] + ] + ) + + MEETING_MAIL_SUBJECT: Any = None + """ + Formulation du titre du mail d’invitation à une réunion. Par défaut s’adapte à ``MEETING_KEY_WORDING``. + """ + + @field_validator("MEETING_MAIL_SUBJECT") + def get_meeting_mail_subject( + cls, meeting_mail_subject: Any, info: FieldValidationInfo + ) -> Any: + return ( + meeting_mail_subject + or AVAILABLE_WORDINGS["MEETING_MAIL_SUBJECT"][ + info.data["MEETING_KEY_WORDING"] + ] + ) + + WORDING_MEETING_PRESENTATION: str = "présentation" + """ + Formulation de « présentation » qui désigne les fichiers accompagnant les réunions. + """ + + WORDING_UPLOAD_FILE: str = "envoyer" + """ + Semble inutilisé. + """ + + FILE_SHARING: bool = False + """ + Active la fonctionnalité de téléversement de fichiers. + """ + + DOCUMENTATION_PAGE_SUBTITLE: Optional[str] = None + """ + Sous-titre de la page de documentation. + """ + + @computed_field() + def WORDINGS(self) -> Dict[str, Any]: + return { + "a_meeting": self.WORDING_A_MEETING, + "the_meeting": self.WORDING_THE_MEETING, + "some_meetings": self.WORDING_MEETINGS, + "of_the_meeting": self.WORDING_OF_THE_MEETING, + "my_meeting": self.WORDING_MY_MEETING, + "this_meeting": self.WORDING_THIS_MEETING, + "meeting_label": self.WORDING_MEETING, + "meeting_presentation": self.WORDING_MEETING_PRESENTATION, + "upload_file_label": self.WORDING_UPLOAD_FILE, + "service_title": self.SERVICE_TITLE, + "service_tagline": self.SERVICE_TAGLINE, + "an_improvised_meeting": self.WORDING_AN_IMPROVISED_MEETING, + "private_meetings": self.WORDING_PRIVATE_MEETINGS, + "a_quick_meeting": self.WORDING_A_QUICK_MEETING, + "good_meeting": self.WORDING_GOOD_MEETING, + "to_the_meeting": self.WORDING_TO_THE_MEETING, + "meeting_undefined_article": self.WORDING_MEETING_UNDEFINED_ARTICLE, + "a_meeting_to_which": self.WORDING_A_MEETING_TO_WHICH, + "welcome_page_subtitle": self.WELCOME_PAGE_SUBTITLE, + "documentation_page_subtitle": self.DOCUMENTATION_PAGE_SUBTITLE, + "meeting_mail_subject": self.MEETING_MAIL_SUBJECT, + } + + QUICK_MEETING: bool = True + """ + Affiche le lien de création de réunions improvisées. + """ + + QUICK_MEETING_DEFAULT_NAME: Optional[str] = None + """ + Nom par défaut des réunions improvisées. + + Par défaut prend la valeur de ``WORDING_IMPROVISED_MEETING``. + """ + + @field_validator("QUICK_MEETING_DEFAULT_NAME") + def get_quick_meeting_default_value( + cls, quick_meeting_default_value: Optional[str], info: FieldValidationInfo + ) -> Any: + return ( + quick_meeting_default_value + or info.data["WORDING_IMPROVISED_MEETING"].capitalize() + ) + + QUICK_MEETING_MODERATOR_LINK_INTRODUCTION: Any = _(" Lien Modérateur ") + """ + Formulation de « Lien Modérateur » dans les liens BBB. + """ + + QUICK_MEETING_ATTENDEE_LINK_INTRODUCTION: Any = _(" Lien Participant ") + """ + Formulation de « Lien Participant » dans les liens BBB. + """ + + QUICK_MEETING_MODERATOR_WELCOME_MESSAGE: Any = None + """ + Formulation du message d’accueil aux modérateurs dans BBB. + Par défaut s’adapte à ``WORDING_THIS_MEETING``. + """ + + @field_validator("QUICK_MEETING_MODERATOR_WELCOME_MESSAGE") + def get_quick_meeting_moderator_welcome_message( + cls, + quick_meeting_moderator_welcome_message: Optional[str], + info: FieldValidationInfo, + ) -> Any: + return quick_meeting_moderator_welcome_message or _( + "Bienvenue aux modérateurs. Pour inviter quelqu'un à %(this_meeting)s, envoyez-lui l'un de ces liens :", + this_meeting=info.data["WORDING_THIS_MEETING"], + ) + + QUICK_MEETING_LOGOUT_URL: Optional[str] = None + """ + Lien vers lequel sont redirigés les participants à la fin d’une réunion improvisée. Par défaut, c’est la page d’accueil du service B3Desk. + """ + + MAIL_MODERATOR_WELCOME_MESSAGE: Any = None + """ + Formulation du message d’accueil aux modérateurs dans BBB, dont le lien à été envoyé par mail. + Par défaut s’adapte à ``WORDING_THIS_MEETING``. + """ + + @field_validator("MAIL_MODERATOR_WELCOME_MESSAGE") + def get_moderator_welcome_message( + cls, moderator_welcome_message: Optional[str], info: FieldValidationInfo + ) -> Any: + return moderator_welcome_message or _( + "Bienvenue. Pour inviter quelqu'un à %(this_meeting)s, envoyez-lui l'un de ces liens :", + this_meeting=info.data["WORDING_THIS_MEETING"], + ) + + MAILTO_LINKS: bool = False + """ + Affiche des liens vers les adresses email des modérateurs et participants dans la liste des réunions. + """ + + SHORTY: bool = False + """ + Affichage court des listes de réunions. + """ + + CLIPBOARD: bool = False + """ + Semble inutilisé. + """ + + RECORDING: bool = False + """ + Active la fonctionnalité d’enregistrement des réunions. + """ + + BETA: bool = False + """ + Active l’encart « Bêta » dans l’entête du service B3Desk. + """ + + MAIL_MEETING: bool = False + """ + Active l’organisation de réunion par envoi de liens par email. + """ + + SMTP_FROM: Optional[str] = None + """ + Adresse email d’expéditeur pour les mails d’invitation. + """ + + SMTP_HOST: Optional[str] = None + """ + Addresse du serveur SMTP. + """ + + SMTP_PORT: Optional[int] = None + """ + Port du serveur SMTP. + """ + + SMTP_USERNAME: Optional[str] = None + """ + Identifiant auprès du serveur SMTP. + """ + + SMTP_PASSWORD: Optional[str] = None + """ + Mot de passe du serveur SMTP. + """ + + SMTP_SSL: Optional[bool] = False + """ + Connexion SSL au serveur SMTP. + """ + + EMAIL_WHITELIST: Any = None + + @field_validator("EMAIL_WHITELIST") + def get_email_whitelist( + cls, email_whitelist: List[str], info: FieldValidationInfo + ) -> str: + if not email_whitelist: + return DEFAULT_EMAIL_WHITELIST + return ( + [email_whitelist] if isinstance(email_whitelist, str) else email_whitelist + ) + + DEFAULT_MEETING_DURATION: int = 280 + """ + Durée maximum en minutes des réunion passée à l'API BBB. + + Plus d’informations sur https://docs.bigbluebutton.org/development/api/#create + """ + + RIE_NETWORK_IPS: Optional[List[str]] = None + """ + Plages d’adresses IP du réseau interministériel de l'État. + + Affiche un encart particulier pour les utilisateurs se connectant depuis ce réseau. + """ + + @field_validator("RIE_NETWORK_IPS", mode="before") + def get_rie_network_ips( + cls, rie_network_ips: List[str], info: FieldValidationInfo + ) -> str: + return ( + rie_network_ips.split(",") + if isinstance(rie_network_ips, str) + else rie_network_ips + ) + + MAX_PARTICIPANTS: int = 200 + """ + Nombre moyen de participants indicatif sur la plateforme. + + Seulement utilisé à des fins d’affichage. + """ + + STATS_CACHE_DURATION: int = 1800 + """ + Durée de rétention du cache des statistiques des réunions. + """ + + STATS_URL: Optional[str] = None + """ + URL du fichier de statistiques des réunions. + + Par exemple ``http://localhost:5000/static/local/stats.csv`` + """ + + STATS_INDEX: int = 2 + """ + Numéro de ligne des statistiques de réunion dans le fichier CSV. + """ + + BIGBLUEBUTTON_ENDPOINT: Optional[str] = None + """ + URL du service BBB. + Par exemple ``https://bbb26.test/bigbluebutton/api`` + """ + + BIGBLUEBUTTON_SECRET: Optional[str] = None + """ + Mot de passe du service BBB. + """ + + BIGBLUEBUTTON_ANALYTICS_CALLBACK_URL: Optional[str] = None + """ + Passé à l'API BBB via le paramètre ``meta_analytics-callback-url``. + Plus d’informations sur https://docs.bigbluebutton.org/development/api/#create + """ From e0a4971d182bee8166f3992c8b2d5b6ab5a950b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89loi=20Rivard?= Date: Thu, 7 Dec 2023 10:36:57 +0100 Subject: [PATCH 3/4] feat: load Flask configuration from pydantic_settings --- pyproject.toml | 1 - web/Dockerfile | 1 - web/Dockerfile-tests | 2 - web/flaskr/__init__.py | 6 +- web/flaskr/models.py | 10 +- web/flaskr/settings.py | 7 +- web/flaskr/utils.py | 6 +- web/instance/config.py | 304 ------------------------------ web/tests/conftest.py | 12 +- web/tests/meeting/test_meeting.py | 2 +- 10 files changed, 27 insertions(+), 324 deletions(-) delete mode 100755 web/instance/config.py diff --git a/pyproject.toml b/pyproject.toml index fba412e9..840c1ede 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -66,7 +66,6 @@ extend-exclude = ''' ''' [tool.pytest.ini_options] -env_files = ["web.env"] testpaths = "web" [tool.ruff] diff --git a/web/Dockerfile b/web/Dockerfile index bfe2e5b2..246d21ac 100644 --- a/web/Dockerfile +++ b/web/Dockerfile @@ -13,7 +13,6 @@ COPY misc/wsgi.py misc/gunicorn.py misc/run_webserver.sh /opt/bbb-visio/ COPY misc/delete_uploaded_files.cron /etc/cron.d/delete_uploaded_files RUN chmod u+x /opt/bbb-visio/run_webserver.sh COPY migrations /opt/bbb-visio/migrations -COPY instance/config.py /opt/bbb-visio/instance/config.py COPY flaskr /opt/bbb-visio/flaskr WORKDIR /opt/bbb-visio/ diff --git a/web/Dockerfile-tests b/web/Dockerfile-tests index 0bfe6132..9c4083d8 100644 --- a/web/Dockerfile-tests +++ b/web/Dockerfile-tests @@ -11,7 +11,5 @@ RUN pip install -r /tmp/requirements.txt COPY requirements.dev.txt /tmp/requirements.dev.txt RUN pip install -r /tmp/requirements.dev.txt -COPY instance/config.py /opt/bbb-visio/instance/config.py - WORKDIR /opt/bbb-visio ENTRYPOINT ["pytest"] diff --git a/web/flaskr/__init__.py b/web/flaskr/__init__.py index 64614b02..37605c33 100755 --- a/web/flaskr/__init__.py +++ b/web/flaskr/__init__.py @@ -29,11 +29,11 @@ def setup_configuration(app, config=None): - app.config.from_pyfile("config.py") if config: app.config.from_mapping(config) - MainSettings.model_validate(config) + config_obj = MainSettings.model_validate(config or {}) + app.config.from_object(config_obj) def setup_cache(app): @@ -114,7 +114,7 @@ def internal_error(error): def create_app(test_config=None, gunicorn_logging=False): - app = Flask(__name__, instance_relative_config=True) + app = Flask(__name__) setup_configuration(app, test_config) setup_cache(app) setup_logging(app, gunicorn_logging) diff --git a/web/flaskr/models.py b/web/flaskr/models.py index 00c8e538..472bd971 100755 --- a/web/flaskr/models.py +++ b/web/flaskr/models.py @@ -205,7 +205,7 @@ def get_params_with_checksum(self, action, params): bigbluebutton_secret = current_app.config["BIGBLUEBUTTON_SECRET"] s = "{}{}".format( pr.url.replace("?", "").replace( - current_app.config["BIGBLUEBUTTON_ENDPOINT"] + "/", "" + f'{current_app.config["BIGBLUEBUTTON_ENDPOINT"]}/', "" ), bigbluebutton_secret, ) @@ -316,7 +316,7 @@ def create(self): if param := self.meeting.maxParticipants: params["maxParticipants"] = str(param) if param := self.meeting.logoutUrl: - params["logoutURL"] = param + params["logoutURL"] = str(param) if param := self.meeting.duration: params["duration"] = str(param) bigbluebutton_analytics_callback_url = current_app.config[ @@ -327,7 +327,9 @@ def create(self): params.update( { "meetingKeepEvents": "true", - "meta_analytics-callback-url": bigbluebutton_analytics_callback_url, + "meta_analytics-callback-url": str( + bigbluebutton_analytics_callback_url + ), } ) if self.meeting.attendeePW is None: @@ -413,7 +415,7 @@ def create(self): pr = request.prepare() bigbluebutton_secret = BIGBLUEBUTTON_SECRET s = "{}{}".format( - pr.url.replace("?", "").replace(BIGBLUEBUTTON_ENDPOINT + "/", ""), + pr.url.replace("?", "").replace(f"{BIGBLUEBUTTON_ENDPOINT}/", ""), bigbluebutton_secret, ) params["checksum"] = hashlib.sha1(s.encode("utf-8")).hexdigest() diff --git a/web/flaskr/settings.py b/web/flaskr/settings.py index 8a8bc113..d1292543 100644 --- a/web/flaskr/settings.py +++ b/web/flaskr/settings.py @@ -491,12 +491,13 @@ def get_attendee_attendee_scopes( Semble inutilisé. """ - @computed_field() + @computed_field def DOCUMENTATION_LINK(self) -> Dict[str, Any]: return { "url": self.DOCUMENTATION_LINK_URL, "label": self.DOCUMENTATION_LINK_LABEL, - "is_external": self.DOCUMENTATION_LINK_URL.lower().startswith( + "is_external": self.DOCUMENTATION_LINK_URL + and self.DOCUMENTATION_LINK_URL.lower().startswith( ("/", self.SERVER_FQDN.lower()) ), } @@ -811,7 +812,7 @@ def get_meeting_mail_subject( Sous-titre de la page de documentation. """ - @computed_field() + @computed_field def WORDINGS(self) -> Dict[str, Any]: return { "a_meeting": self.WORDING_A_MEETING, diff --git a/web/flaskr/utils.py b/web/flaskr/utils.py index 849f0224..544419d0 100644 --- a/web/flaskr/utils.py +++ b/web/flaskr/utils.py @@ -16,8 +16,8 @@ def is_rie(): if not request.remote_addr: return False - return any( - IPAddress(request.remote_addr) in IPNetwork(network_ip) - for network_ip in current_app.config.get("RIE_NETWORK_IPS", []) + return current_app.config["RIE_NETWORK_IPS"] and any( + IPAddress(request.remote_addr) in IPNetwork(str(network_ip)) + for network_ip in current_app.config["RIE_NETWORK_IPS"] if network_ip ) diff --git a/web/instance/config.py b/web/instance/config.py deleted file mode 100755 index a7073361..00000000 --- a/web/instance/config.py +++ /dev/null @@ -1,304 +0,0 @@ -import json -import os - -from flask_babel import lazy_gettext -from flaskr.constants import DEFAULT_EMAIL_WHITELIST - - -# App configuration -SECRET_KEY = os.environ.get("SECRET_KEY") -NC_LOGIN_TIMEDELTA_DAYS = int(os.environ.get("NC_LOGIN_TIMEDELTA_DAYS")) -REDIS_URL = os.environ.get("REDIS_URL") -NC_LOGIN_API_URL = os.environ.get("NC_LOGIN_API_URL") -NC_LOGIN_API_KEY = os.environ.get("NC_LOGIN_API_KEY") -UPLOAD_DIR = os.environ.get("UPLOAD_DIR") -TMP_DOWNLOAD_DIR = os.environ.get("TMP_DOWNLOAD_DIR") -MAX_SIZE_UPLOAD = os.environ.get("MAX_SIZE_UPLOAD") -TIME_FORMAT = os.environ.get("TIME_FORMAT") -TESTING = True -DEBUG = True -TITLE = os.environ.get("TITLE") -SERVER_FQDN = os.environ.get("SERVER_FQDN") -EXTERNAL_UPLOAD_DESCRIPTION = os.environ.get("EXTERNAL_UPLOAD_DESCRIPTION") -WTF_CSRF_TIME_LIMIT = int(os.environ.get("WTF_CSRF_TIME_LIMIT", 3600 * 12)) -MAX_MEETINGS_PER_USER = int(os.environ.get("MAX_MEETINGS_PER_USER", 50)) - -ALLOWED_MIME_TYPES_SERVER_SIDE = json.loads( - os.environ.get("ALLOWED_MIME_TYPES_SERVER_SIDE", "[]") or "[]" -) -ACCEPTED_FILES_CLIENT_SIDE = os.environ.get("ACCEPTED_FILES_CLIENT_SIDE", "") - -# Default OIDC Configuration -OIDC_ID_TOKEN_COOKIE_SECURE = False -OIDC_REQUIRE_VERIFIED_EMAIL = False -OIDC_USER_INFO_ENABLED = True -OIDC_OPENID_REALM = os.environ.get("OIDC_OPENID_REALM") -OIDC_SCOPES = ( - list(map(str.strip, os.environ["OIDC_SCOPES"].split(","))) - if os.environ.get("OIDC_SCOPES") - else [ - "openid", - "email", - "profile", - ] -) -OIDC_INTROSPECTION_AUTH_METHOD = "client_secret_post" -OIDC_USERINFO_HTTP_METHOD = os.environ.get("OIDC_USERINFO_HTTP_METHOD") -OIDC_INFO_REQUESTED_FIELDS = ["email", "given_name", "family_name"] -OIDC_ISSUER = os.environ.get("OIDC_ISSUER") -OIDC_AUTH_URI = os.environ.get("OIDC_AUTH_URI") -OIDC_USERINFO_URI = os.environ.get("OIDC_USERINFO_URI") -OIDC_TOKEN_URI = os.environ.get("OIDC_TOKEN_URI") -OIDC_CLIENT_ID = os.environ.get("OIDC_CLIENT_ID") -OIDC_CLIENT_SECRET = os.environ.get("OIDC_CLIENT_SECRET") -OIDC_CLIENT_AUTH_METHOD = os.environ.get("OIDC_CLIENT_AUTH_METHOD") -OIDC_REDIRECT_URI = os.environ.get("OIDC_REDIRECT_URI") -OIDC_SERVICE_NAME = os.environ.get("OIDC_SERVICE_NAME") - -# Attendee OIDC Configuration (back to default if empty) -OIDC_ATTENDEE_ENABLED = os.environ.get("OIDC_ATTENDEE_ENABLED") not in [ - 0, - False, - "0", - "false", - "False", - "off", - "OFF", -] -OIDC_ATTENDEE_ISSUER = os.environ.get("OIDC_ATTENDEE_ISSUER") or OIDC_ISSUER -OIDC_ATTENDEE_CLIENT_ID = os.environ.get("OIDC_ATTENDEE_CLIENT_ID") or OIDC_CLIENT_ID -OIDC_ATTENDEE_CLIENT_SECRET = ( - os.environ.get("OIDC_ATTENDEE_CLIENT_SECRET") or OIDC_CLIENT_SECRET -) -OIDC_ATTENDEE_CLIENT_AUTH_METHOD = ( - os.environ.get("OIDC_ATTENDEE_CLIENT_AUTH_METHOD") or OIDC_CLIENT_AUTH_METHOD -) -OIDC_ATTENDEE_USERINFO_HTTP_METHOD = ( - os.environ.get("OIDC_ATTENDEE_USERINFO_HTTP_METHOD") or OIDC_USERINFO_HTTP_METHOD -) -OIDC_ATTENDEE_SERVICE_NAME = ( - os.environ.get("OIDC_ATTENDEE_SERVICE_NAME") or OIDC_SERVICE_NAME -) -OIDC_ATTENDEE_SCOPES = ( - list(map(str.strip, os.environ["OIDC_ATTENDEE_SCOPES"].split(","))) - if os.environ.get("OIDC_ATTENDEE_SCOPES") - else OIDC_SCOPES -) - -# Links -DOCUMENTATION_LINK = { - "url": os.environ.get("DOCUMENTATION_LINK_URL"), - "label": os.environ.get("DOCUMENTATION_LINK_LABEL"), - "is_external": not os.environ.get("DOCUMENTATION_LINK_URL") - .lower() - .startswith(("/", SERVER_FQDN.lower())), -} -SERVICE_TITLE = os.environ.get("SERVICE_TITLE") -SERVICE_TAGLINE = os.environ.get("SERVICE_TAGLINE") - -MEETING_LOGOUT_URL = os.environ.get("MEETING_LOGOUT_URL", "") -SATISFACTION_POLL_URL = os.environ.get("SATISFACTION_POLL_URL") - -# Database configuration -SQLALCHEMY_DATABASE_URI = os.environ.get("SQLALCHEMY_DATABASE_URI") -SQLALCHEMY_TRACK_MODIFICATIONS = False - -# wording -MEETING_KEY_WORDING = os.environ.get("MEETING_KEY_WORDING", "reunion") - -AVAILABLE_WORDINGS = { - "MEETING": {"cours": "cours", "reunion": "réunion", "seminaire": "séminaire"}, - "MEETINGS": {"cours": "cours", "reunion": "réunions", "seminaire": "séminaires"}, - "A_MEETING": { - "cours": "un cours", - "reunion": "une réunion", - "seminaire": "un séminaire", - }, - "MY_MEETING": { - "cours": "mon cours", - "reunion": "ma réunion", - "seminaire": "mon séminaire", - }, - "THE_MEETING": { - "cours": "le cours", - "reunion": "la réunion", - "seminaire": "le séminaire", - }, - "OF_THE_MEETING": { - "cours": "du cours", - "reunion": "de la réunion", - "seminaire": "du séminaire", - }, - "THIS_MEETING": { - "cours": "ce cours", - "reunion": "cette réunion", - "seminaire": "ce séminaire", - }, - "TO_THE_MEETING": { - "cours": "au cours", - "reunion": "à la réunion", - "seminaire": "au séminaire", - }, - "IMPROVISED_MEETING": { - "cours": "cours improvisé", - "reunion": "réunion improvisée", - "seminaire": "séminaire improvisé", - }, - "AN_IMPROVISED_MEETING": { - "cours": "un cours improvisé", - "reunion": lazy_gettext("une réunion improvisée"), - "seminaire": "un séminaire improvisé", - }, - "A_QUICK_MEETING": { - "cours": "un cours immédiat", - "reunion": lazy_gettext("une réunion immédiate"), - "seminaire": "un séminaire immédiat", - }, - "PRIVATE_MEETINGS": { - "cours": "cours privés", - "reunion": lazy_gettext("réunions privées"), - "seminaire": "séminaires privés", - }, - "GOOD_MEETING": { - "cours": "bon cours", - "reunion": lazy_gettext("bonne réunion"), - "seminaire": "bon séminaire", - }, - "MEETING_UNDEFINED_ARTICLE": { - "cours": "un", - "reunion": lazy_gettext("une"), - "seminaire": "un", - }, - "A_MEETING_TO_WHICH": { - "cours": "un cours auquel", - "reunion": lazy_gettext("une réunion à laquelle"), - "seminaire": "un séminaire auquel", - }, - "WELCOME_PAGE_SUBTITLE": { - "cours": lazy_gettext( - "Créez un cours immédiatement avec des réglages standards. Ce cours ne sera pas enregistré dans votre liste de salons." - ), - "reunion": lazy_gettext( - "Créez une réunion immédiatement avec des réglages standards. Cette réunion ne sera pas enregistrée dans votre liste de salons." - ), - "seminaire": lazy_gettext( - "Créez un séminaire immédiatement avec des réglages standards. Ce séminaire ne sera pas enregistré dans votre liste de salons." - ), - }, - "MEETING_MAIL_SUBJECT": { - "cours": lazy_gettext( - "Invitation à un cours en ligne immédiat du Webinaire de l’Etat" - ), - "reunion": lazy_gettext( - "Invitation à une réunion en ligne immédiat du Webinaire de l’Etat" - ), - "seminaire": lazy_gettext( - "Invitation à un séminaire en ligne immédiat du Webinaire de l’Etat" - ), - }, -} - - -WORDING_A_MEETING = AVAILABLE_WORDINGS["A_MEETING"][MEETING_KEY_WORDING] -WORDING_MY_MEETING = AVAILABLE_WORDINGS["MY_MEETING"][MEETING_KEY_WORDING] -WORDING_THE_MEETING = AVAILABLE_WORDINGS["THE_MEETING"][MEETING_KEY_WORDING] -WORDING_OF_THE_MEETING = AVAILABLE_WORDINGS["OF_THE_MEETING"][MEETING_KEY_WORDING] -WORDING_MEETING = AVAILABLE_WORDINGS["MEETING"][MEETING_KEY_WORDING] -WORDING_MEETINGS = AVAILABLE_WORDINGS["MEETINGS"][MEETING_KEY_WORDING] -WORDING_THIS_MEETING = AVAILABLE_WORDINGS["THIS_MEETING"][MEETING_KEY_WORDING] -WORDING_TO_THE_MEETING = AVAILABLE_WORDINGS["TO_THE_MEETING"][MEETING_KEY_WORDING] -WORDING_IMPROVISED_MEETING = AVAILABLE_WORDINGS["IMPROVISED_MEETING"][ - MEETING_KEY_WORDING -] -WORDING_AN_IMPROVISED_MEETING = AVAILABLE_WORDINGS["AN_IMPROVISED_MEETING"][ - MEETING_KEY_WORDING -] -WORDING_A_QUICK_MEETING = AVAILABLE_WORDINGS["A_QUICK_MEETING"][MEETING_KEY_WORDING] -WORDING_PRIVATE_MEETINGS = AVAILABLE_WORDINGS["PRIVATE_MEETINGS"][MEETING_KEY_WORDING] -WORDING_GOOD_MEETING = AVAILABLE_WORDINGS["GOOD_MEETING"][MEETING_KEY_WORDING] -WORDING_MEETING_UNDEFINED_ARTICLE = AVAILABLE_WORDINGS["MEETING_UNDEFINED_ARTICLE"][ - MEETING_KEY_WORDING -] -WORDING_A_MEETING_TO_WHICH = AVAILABLE_WORDINGS["A_MEETING_TO_WHICH"][ - MEETING_KEY_WORDING -] -WELCOME_PAGE_SUBTITLE = AVAILABLE_WORDINGS["WELCOME_PAGE_SUBTITLE"][MEETING_KEY_WORDING] -MEETING_MAIL_SUBJECT = AVAILABLE_WORDINGS["MEETING_MAIL_SUBJECT"][MEETING_KEY_WORDING] - -WORDING_MEETING_PRESENTATION = os.environ.get( - "WORDING_MEETING_PRESENTATION", "présentation" -) -WORDING_UPLOAD_FILE = os.environ.get("WORDING_MEETING_UPLOAD_FILE", "envoyer") - -FILE_SHARING = os.environ.get("FILE_SHARING") == "on" - -DOCUMENTATION_PAGE_SUBTITLE = os.environ.get("DOCUMENTATION_PAGE_SUBTITLE") -WORDINGS = { - "a_meeting": WORDING_A_MEETING, - "the_meeting": WORDING_THE_MEETING, - "some_meetings": WORDING_MEETINGS, - "of_the_meeting": WORDING_OF_THE_MEETING, - "my_meeting": WORDING_MY_MEETING, - "this_meeting": WORDING_THIS_MEETING, - "meeting_label": WORDING_MEETING, - "meeting_presentation": WORDING_MEETING_PRESENTATION, - "upload_file_label": WORDING_UPLOAD_FILE, - "service_title": SERVICE_TITLE, - "service_tagline": SERVICE_TAGLINE, - "an_improvised_meeting": WORDING_AN_IMPROVISED_MEETING, - "private_meetings": WORDING_PRIVATE_MEETINGS, - "a_quick_meeting": WORDING_A_QUICK_MEETING, - "good_meeting": WORDING_GOOD_MEETING, - "to_the_meeting": WORDING_TO_THE_MEETING, - "meeting_undefined_article": WORDING_MEETING_UNDEFINED_ARTICLE, - "a_meeting_to_which": WORDING_A_MEETING_TO_WHICH, - "welcome_page_subtitle": WELCOME_PAGE_SUBTITLE, - "documentation_page_subtitle": DOCUMENTATION_PAGE_SUBTITLE, - "meeting_mail_subject": MEETING_MAIL_SUBJECT, -} - -# quick meeting -QUICK_MEETING = True -QUICK_MEETING_DEFAULT_NAME = WORDING_IMPROVISED_MEETING.capitalize() -QUICK_MEETING_MODERATOR_LINK_INTRODUCTION = lazy_gettext(" Lien Modérateur ") -QUICK_MEETING_ATTENDEE_LINK_INTRODUCTION = lazy_gettext(" Lien Participant ") -QUICK_MEETING_MODERATOR_WELCOME_MESSAGE = lazy_gettext( - "Bienvenue aux modérateurs. Pour inviter quelqu'un à %(this_meeting)s, envoyez-lui l'un de ces liens :", - this_meeting=WORDING_THIS_MEETING, -) -QUICK_MEETING_LOGOUT_URL = os.environ.get("QUICK_MEETING_LOGOUT_URL") -MAIL_MODERATOR_WELCOME_MESSAGE = lazy_gettext( - "Bienvenue. Pour inviter quelqu'un à %(this_meeting)s, envoyez-lui l'un de ces liens :", - this_meeting=WORDING_THIS_MEETING, -) -MAILTO_LINKS = False -SHORTY = False -CLIPBOARD = os.environ.get("CLIPBOARD") == "on" -RECORDING = os.environ.get("RECORDING") == "on" -BETA = os.environ.get("BETA") == "on" -MAIL_MEETING = os.environ.get("MAIL_MEETING") == "on" -SMTP_FROM = os.environ.get("SMTP_FROM") -SMTP_HOST = os.environ.get("SMTP_HOST") -SMTP_PORT = os.environ.get("SMTP_PORT") -SMTP_USERNAME = os.environ.get("SMTP_USERNAME") -SMTP_PASSWORD = os.environ.get("SMTP_PASSWORD") -SMTP_SSL = os.environ.get("SMTP_SSL") -EMAIL_WHITELIST = os.environ.get("EMAIL_WHITELIST") -DEFAULT_MEETING_DURATION = os.environ.get("DEFAULT_MEETING_DURATION", 280) - -RIE_NETWORK_IPS = os.environ.get("RIE_NETWORK_IPS", "").split(",") -MAX_PARTICIPANTS = os.environ.get("MAX_PARTICIPANTS", 200) -STATS_CACHE_DURATION = int(os.environ.get("STATS_CACHE_DURATION", 1800)) -STATS_URL = os.environ.get("STATS_URL") -STATS_INDEX = int(os.environ.get("STATS_INDEX", 2)) - -# Big Blue Button configuration -BIGBLUEBUTTON_ENDPOINT = os.environ.get("BIGBLUEBUTTON_ENDPOINT") -BIGBLUEBUTTON_SECRET = os.environ.get("BIGBLUEBUTTON_SECRET") -BIGBLUEBUTTON_ANALYTICS_CALLBACK_URL = os.environ.get( - "BIGBLUEBUTTON_ANALYTICS_CALLBACK_URL" -) - -BABEL_TRANSLATION_DIRECTORIES = "/opt/bbb-visio/translations" - -EMAIL_WHITELIST = DEFAULT_EMAIL_WHITELIST diff --git a/web/tests/conftest.py b/web/tests/conftest.py index 65bff33d..9bda9f3a 100644 --- a/web/tests/conftest.py +++ b/web/tests/conftest.py @@ -43,15 +43,23 @@ def wrapper(*args, **kwargs): @pytest.fixture() -def app(mocker): +def app(mocker, tmp_path): mocker.patch("flask_pyoidc.OIDCAuthentication", return_value=FakeAuth()) app = create_app( test_config={ + "SECRET_KEY": "test-secret-key", + "SERVER_FQDN": "http://localhost:5000", "SQLALCHEMY_DATABASE_URI": "sqlite:///:memory:", "WTF_CSRF_ENABLED": False, "TESTING": True, "BIGBLUEBUTTON_ENDPOINT": "https://bbb.test", - "OIDC_ATTENDEE_ISSUER": "http://oidc-server.test", + "OIDC_ISSUER": "http://oidc-server.test", + "UPLOAD_DIR": str(tmp_path), + "TMP_DOWNLOAD_DIR": str(tmp_path), + "RECORDING": True, + "BIGBLUEBUTTON_ANALYTICS_CALLBACK_URL": "https://bbb-analytics-staging.osc-fr1.scalingo.io/v1/post_events", + "MEETING_KEY_WORDING": "seminaire", + "QUICK_MEETING_LOGOUT_URL": "http://education.gouv.fr/", } ) with app.app_context(): diff --git a/web/tests/meeting/test_meeting.py b/web/tests/meeting/test_meeting.py index 126345a5..f37f8e4a 100644 --- a/web/tests/meeting/test_meeting.py +++ b/web/tests/meeting/test_meeting.py @@ -321,7 +321,7 @@ class Resp: "uploadExternalDescription": "Fichiers depuis votre Nextcloud", "attendeePW": meeting.attendeePW, "moderatorPW": meeting.moderatorPW, - "logoutURL": "http://education.gouv.fr", + "logoutURL": "http://education.gouv.fr/", "duration": "280", "meetingKeepEvents": "true", "meta_analytics-callback-url": "https://bbb-analytics-staging.osc-fr1.scalingo.io/v1/post_events", From 1d8317be4f4eafa4a0719f21cddbb296613dba9a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89loi=20Rivard?= Date: Thu, 7 Dec 2023 16:48:48 +0100 Subject: [PATCH 4/4] feat: documentation generation --- documentation/conf.py | 11 +++++++++++ documentation/maintainers/index.rst | 1 + documentation/maintainers/settings.rst | 4 ++++ poetry.lock | 25 ++++++++++++++++++++++++- pyproject.toml | 1 + web/requirements.doc.txt | 7 +++++++ 6 files changed, 48 insertions(+), 1 deletion(-) create mode 100644 documentation/maintainers/settings.rst diff --git a/documentation/conf.py b/documentation/conf.py index 3bdf34b2..5c8d4a04 100644 --- a/documentation/conf.py +++ b/documentation/conf.py @@ -20,6 +20,7 @@ "sphinx.ext.intersphinx", "sphinx.ext.todo", "sphinx.ext.viewcode", + "sphinxcontrib.autodoc_pydantic", ] templates_path = ["_templates"] @@ -90,3 +91,13 @@ # -- Options for autosectionlabel ----------------------------------------- autosectionlabel_prefix_document = True + +# -- Options for autodo_pydantic_settings ------------------------------------------- + +autodoc_pydantic_model_show_json = False +autodoc_pydantic_model_show_config_summary = False +autodoc_pydantic_model_show_config_summary = False +autodoc_pydantic_model_show_validator_summary = False +autodoc_pydantic_model_show_validator_members = False +autodoc_pydantic_model_show_field_summary = False +autodoc_pydantic_field_list_validators = False diff --git a/documentation/maintainers/index.rst b/documentation/maintainers/index.rst index 7c9a76e2..3a3f5faa 100644 --- a/documentation/maintainers/index.rst +++ b/documentation/maintainers/index.rst @@ -7,4 +7,5 @@ Cette partie de la documentation est à destination des personnes qui déploient :maxdepth: 2 pullRequestValidation + settings meetingFiles diff --git a/documentation/maintainers/settings.rst b/documentation/maintainers/settings.rst new file mode 100644 index 00000000..6c5da327 --- /dev/null +++ b/documentation/maintainers/settings.rst @@ -0,0 +1,4 @@ +Configuration +############# + +.. autopydantic_model:: flaskr.settings.MainSettings diff --git a/poetry.lock b/poetry.lock index 577f2261..afa294ab 100644 --- a/poetry.lock +++ b/poetry.lock @@ -71,6 +71,29 @@ files = [ {file = "async_timeout-4.0.3-py3-none-any.whl", hash = "sha256:7405140ff1230c310e51dc27b3145b9092d659ce68ff733fb0cefe3ee42be028"}, ] +[[package]] +name = "autodoc-pydantic" +version = "2.0.1" +description = "Seamlessly integrate pydantic models in your Sphinx documentation." +optional = false +python-versions = ">=3.7.1,<4.0.0" +files = [ + {file = "autodoc_pydantic-2.0.1-py3-none-any.whl", hash = "sha256:d3c302fdb6d37edb5b721f0f540252fa79cea7018bc1a9a85bf70f33a68b0ce4"}, + {file = "autodoc_pydantic-2.0.1.tar.gz", hash = "sha256:7a125a4ff18e4903e27be71e4ddb3269380860eacab4a584d6cc2e212fa96991"}, +] + +[package.dependencies] +importlib-metadata = {version = ">1", markers = "python_version <= \"3.8\""} +pydantic = ">=2.0,<3.0.0" +pydantic-settings = ">=2.0,<3.0.0" +Sphinx = ">=4.0" + +[package.extras] +dev = ["coverage (>=7,<8)", "flake8 (>=3,<4)", "pytest (>=7,<8)", "sphinx-copybutton (>=0.4,<0.5)", "sphinx-rtd-theme (>=1.0,<2.0)", "sphinx-tabs (>=3,<4)", "sphinxcontrib-mermaid (>=0.7,<0.8)", "tox (>=3,<4)"] +docs = ["sphinx-copybutton (>=0.4,<0.5)", "sphinx-rtd-theme (>=1.0,<2.0)", "sphinx-tabs (>=3,<4)", "sphinxcontrib-mermaid (>=0.7,<0.8)"] +erdantic = ["erdantic (>=0.6,<0.7)"] +test = ["coverage (>=7,<8)", "pytest (>=7,<8)"] + [[package]] name = "babel" version = "2.13.1" @@ -2629,4 +2652,4 @@ testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "p [metadata] lock-version = "2.0" python-versions = ">=3.8.1,<4.0" -content-hash = "821efb9756c168ad7eec23c00b864275d14affa92cae2f940ca0667ad55e9b4e" +content-hash = "1c672e161ffc2f1472e7464015a7cb3e57310df856521644ce11ad21426501ee" diff --git a/pyproject.toml b/pyproject.toml index 840c1ede..da3b82b2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -56,6 +56,7 @@ optional = true sphinx = "^7.0.0" sphinx-rtd-theme = "^2.0.0" sphinx-issues = "^3.0.0" +autodoc-pydantic = "^2.0.1" myst-parser = "^2.0.0" [tool.black] diff --git a/web/requirements.doc.txt b/web/requirements.doc.txt index 35017f42..6e3e579d 100644 --- a/web/requirements.doc.txt +++ b/web/requirements.doc.txt @@ -1,4 +1,6 @@ alabaster==0.7.13 ; python_full_version >= "3.8.1" and python_version < "4.0" +annotated-types==0.6.0 ; python_full_version >= "3.8.1" and python_version < "4.0" +autodoc-pydantic==2.0.1 ; python_full_version >= "3.8.1" and python_version < "4.0" babel==2.13.1 ; python_full_version >= "3.8.1" and python_version < "4.0" certifi==2023.11.17 ; python_full_version >= "3.8.1" and python_version < "4.0" charset-normalizer==3.3.2 ; python_full_version >= "3.8.1" and python_version < "4.0" @@ -14,7 +16,11 @@ mdit-py-plugins==0.4.0 ; python_full_version >= "3.8.1" and python_version < "4. mdurl==0.1.2 ; python_full_version >= "3.8.1" and python_version < "4.0" myst-parser==2.0.0 ; python_full_version >= "3.8.1" and python_version < "4.0" packaging==23.2 ; python_full_version >= "3.8.1" and python_version < "4.0" +pydantic-core==2.14.5 ; python_full_version >= "3.8.1" and python_version < "4.0" +pydantic-settings==2.1.0 ; python_full_version >= "3.8.1" and python_version < "4.0" +pydantic==2.5.2 ; python_full_version >= "3.8.1" and python_version < "4.0" pygments==2.17.2 ; python_full_version >= "3.8.1" and python_version < "4.0" +python-dotenv==1.0.0 ; python_full_version >= "3.8.1" and python_version < "4.0" pytz==2023.3.post1 ; python_full_version >= "3.8.1" and python_version < "3.9" pyyaml==6.0.1 ; python_full_version >= "3.8.1" and python_version < "4.0" requests==2.31.0 ; python_full_version >= "3.8.1" and python_version < "4.0" @@ -30,5 +36,6 @@ sphinxcontrib-jquery==4.1 ; python_full_version >= "3.8.1" and python_version < sphinxcontrib-jsmath==1.0.1 ; python_full_version >= "3.8.1" and python_version < "4.0" sphinxcontrib-qthelp==1.0.3 ; python_full_version >= "3.8.1" and python_version < "4.0" sphinxcontrib-serializinghtml==1.1.5 ; python_full_version >= "3.8.1" and python_version < "4.0" +typing-extensions==4.8.0 ; python_full_version >= "3.8.1" and python_version < "4.0" urllib3==2.1.0 ; python_full_version >= "3.8.1" and python_version < "4.0" zipp==3.17.0 ; python_full_version >= "3.8.1" and python_version < "3.10"