diff --git a/VERSION b/VERSION new file mode 100644 index 0000000..8a9ecc2 --- /dev/null +++ b/VERSION @@ -0,0 +1 @@ +0.0.1 \ No newline at end of file diff --git a/backend/app/database.py b/backend/app/database.py deleted file mode 100644 index cc45acb..0000000 --- a/backend/app/database.py +++ /dev/null @@ -1,2 +0,0 @@ -from flask_sqlalchemy import SQLAlchemy -db = SQLAlchemy() diff --git a/backend/config.py.sample b/backend/config.py.sample index efea5a9..d5dd9d4 100644 --- a/backend/config.py.sample +++ b/backend/config.py.sample @@ -8,3 +8,8 @@ SQLALCHEMY_TRACK_MODIFICATIONS = False ENABLE_NOMENCLATURE_TAXONOMIC_FILTERS = True ID_ATTR_TAXHUB = 50018 + +SESSION_TYPE = 'filesystem' +SECRET_KEY = 'super secret key' +COOKIE_EXPIRATION = 3600 +COOKIE_AUTORENEW = True \ No newline at end of file diff --git a/backend/app/__init__.py b/backend/flora_occitania/__init__.py similarity index 100% rename from backend/app/__init__.py rename to backend/flora_occitania/__init__.py diff --git a/backend/flora_occitania/database.py b/backend/flora_occitania/database.py new file mode 100644 index 0000000..fb66924 --- /dev/null +++ b/backend/flora_occitania/database.py @@ -0,0 +1,17 @@ +from os import environ +from importlib import import_module + +from flask_marshmallow import Marshmallow +from flask_sqlalchemy import SQLAlchemy + + +db_path = environ.get("FLASK_SQLALCHEMY_DB") +if db_path: + db_module_name, db_object_name = db_path.rsplit(".", 1) + db_module = import_module(db_module_name) + db = getattr(db_module, db_object_name) +else: + db = SQLAlchemy() + environ["FLASK_SQLALCHEMY_DB"] = "flora_occitania.database.db" + +import os diff --git a/backend/app/models.py b/backend/flora_occitania/models.py similarity index 85% rename from backend/app/models.py rename to backend/flora_occitania/models.py index e740507..2549269 100644 --- a/backend/app/models.py +++ b/backend/flora_occitania/models.py @@ -1,26 +1,21 @@ """ - Définition des models +Définition des models """ + from sqlalchemy import ForeignKey from sqlalchemy.orm import relationship from sqlalchemy.dialects.postgresql import ARRAY -from app.database import db -from app.utils import serializable +from flora_occitania.database import db +from flora_occitania.utils import serializable @serializable class CorTaxonAttribut(db.Model): __tablename__ = "cor_taxon_attribut" - __table_args__ = {"schema": "taxonomie"} - id_attribut = db.Column( - db.Integer, - primary_key=True - ) - cd_ref = db.Column( - db.Integer, - primary_key=True - ) + __table_args__ = {"schema": "taxonomie", "extend_existing": True} + id_attribut = db.Column(db.Integer, primary_key=True) + cd_ref = db.Column(db.Integer, primary_key=True) valeur_attribut = db.Column(db.Text, nullable=False) @@ -52,7 +47,6 @@ class NomVern(db.Model): meta_update_date = db.Column(db.DateTime) - @serializable class ListTaxon(db.Model): __tablename__ = "v_list_summary_taxon_to_fill" @@ -72,5 +66,5 @@ class ListTaxon(db.Model): "NomVern", backref="group", primaryjoin="NomVern.cd_ref == ListTaxon.cd_ref", - foreign_keys="NomVern.cd_ref" + foreign_keys="NomVern.cd_ref", ) diff --git a/backend/app/routes.py b/backend/flora_occitania/routes.py similarity index 79% rename from backend/app/routes.py rename to backend/flora_occitania/routes.py index 095493a..cd136f2 100644 --- a/backend/app/routes.py +++ b/backend/flora_occitania/routes.py @@ -2,9 +2,11 @@ Routes correspondantes à l'API de flora occitania GET/POST """ -from flask import Blueprint, request, current_app +from flask import Blueprint, request, current_app, g from sqlalchemy.orm.exc import NoResultFound +from pypnusershub.routes import check_auth + from .models import ( NomVern, ListTaxon, Sources, CorTaxonAttribut ) @@ -62,11 +64,13 @@ def get_taxon_list(id=None): } -@adresses.route('/', methods=['POST']) +@adresses.route("/", methods=["POST"]) +@check_auth(1) def post_taxon_nomVern(cd_ref): """ Sauvegarde un nom vernaculaire """ + id_role = g.current_user.id_role data = request.json lst_nom_verns = data['params']['nomVerns'] @@ -74,25 +78,25 @@ def post_taxon_nomVern(cd_ref): # Insertion du commentaire général dans taxhub if 'commentaire_general' in data['params']: + cmt = data['params']['commentaire_general'] - try: - taxhub_attr = db.session.query( - CorTaxonAttribut - ).filter_by( - cd_ref=cd_ref, - id_attribut=current_app.config['ID_ATTR_TAXHUB'] - ).one() - except NoResultFound: - taxhub_attr = CorTaxonAttribut( - cd_ref=cd_ref, - id_attribut=current_app.config['ID_ATTR_TAXHUB'] - ) - taxhub_attr.valeur_attribut = cmt - - print(taxhub_attr.valeur_attribut) - - db.session.add(taxhub_attr) - db.session.commit() + if cmt: + try: + taxhub_attr = db.session.query( + CorTaxonAttribut + ).filter_by( + cd_ref=cd_ref, + id_attribut=current_app.config['ID_ATTR_TAXHUB'] + ).one() + except NoResultFound: + taxhub_attr = CorTaxonAttribut( + cd_ref=cd_ref, + id_attribut=current_app.config['ID_ATTR_TAXHUB'] + ) + taxhub_attr.valeur_attribut = cmt + + db.session.add(taxhub_attr) + db.session.commit() # ########################## # Insertion des noms vernaculaires diff --git a/backend/app/utils.py b/backend/flora_occitania/utils.py similarity index 100% rename from backend/app/utils.py rename to backend/flora_occitania/utils.py diff --git a/backend/requirements.in b/backend/requirements.in new file mode 100644 index 0000000..f9d94f7 --- /dev/null +++ b/backend/requirements.in @@ -0,0 +1,11 @@ + +pypnusershub +https://github.com/PnX-SI/Nomenclature-api-module/archive/1.6.4.zip +Click +flask>=3.0 +flask-cors +flask-sqlalchemy +psycopg2 +sqlalchemy<2.0 +gunicorn>=19.8.0 + diff --git a/backend/requirements.txt b/backend/requirements.txt index b6813e9..6dbb81a 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -1,15 +1,207 @@ - -https://github.com/PnX-SI/Nomenclature-api-module/archive/1.3.0.zip -Click==7.0 -Flask==1.1.1 -Flask-Cors==3.0.8 -Flask-SQLAlchemy==2.4.0 -itsdangerous==1.1.0 -Jinja2==2.10.1 -MarkupSafe==1.1.1 -psycopg2==2.8.3 -python-dateutil==2.8.0 -six==1.12.0 -SQLAlchemy==1.3.8 -Werkzeug==0.15.6 -gunicorn==19.9.0 \ No newline at end of file +# +# This file is autogenerated by pip-compile with Python 3.10 +# by the following command: +# +# pip-compile requirements.ini +# +alembic==1.15.2 + # via + # flask-migrate + # pypn-ref-geo + # pypnusershub +attrs==25.3.0 + # via fiona +authlib==1.5.2 + # via pypnusershub +bcrypt==4.3.0 + # via pypnusershub +blinker==1.9.0 + # via flask +certifi==2025.4.26 + # via + # fiona + # requests +cffi==1.17.1 + # via cryptography +charset-normalizer==3.4.2 + # via requests +click==8.2.0 + # via + # -r requirements.ini + # click-plugins + # cligj + # fiona + # flask + # taxhub +click-plugins==1.1.1 + # via fiona +cligj==0.7.2 + # via fiona +cryptography==45.0.2 + # via authlib +fiona==1.10.1 + # via utils-flask-sqlalchemy-geo +flask==3.1.1 + # via + # -r requirements.ini + # flask-admin + # flask-cors + # flask-login + # flask-marshmallow + # flask-migrate + # flask-sqlalchemy + # pypn-ref-geo + # pypnnomenclature + # pypnusershub + # taxhub + # utils-flask-sqlalchemy +flask-admin==1.6.1 + # via + # pypnnomenclature + # taxhub +flask-cors==6.0.0 + # via + # -r requirements.ini + # taxhub +flask-login==0.6.3 + # via pypnusershub +flask-marshmallow==1.3.0 + # via + # pypn-ref-geo + # pypnnomenclature + # pypnusershub +flask-migrate==4.1.0 + # via + # pypnnomenclature + # taxhub + # utils-flask-sqlalchemy +flask-sqlalchemy==3.0.5 + # via + # -r requirements.ini + # flask-migrate + # pypn-ref-geo + # pypnnomenclature + # pypnusershub + # taxhub + # utils-flask-sqlalchemy +geoalchemy2==0.17.1 + # via utils-flask-sqlalchemy-geo +geojson==3.2.0 + # via utils-flask-sqlalchemy-geo +greenlet==3.2.2 + # via sqlalchemy +gunicorn==23.0.0 + # via + # -r requirements.ini + # taxhub +idna==3.10 + # via requests +itsdangerous==2.2.0 + # via flask +jinja2==3.1.6 + # via flask +mako==1.3.10 + # via alembic +markupsafe==3.0.2 + # via + # flask + # jinja2 + # mako + # werkzeug + # wtforms +marshmallow==3.26.1 + # via + # flask-marshmallow + # marshmallow-geojson + # marshmallow-sqlalchemy + # utils-flask-sqlalchemy +marshmallow-geojson==0.5.0 + # via utils-flask-sqlalchemy-geo +marshmallow-sqlalchemy==1.4.2 + # via + # pypnnomenclature + # pypnusershub + # taxhub + # utils-flask-sqlalchemy-geo +numpy==2.2.6 + # via shapely +packaging==25.0 + # via + # geoalchemy2 + # gunicorn + # marshmallow +pillow==9.5.0 + # via taxhub +psycopg2==2.9.10 + # via + # -r requirements.ini + # pypn-ref-geo + # pypnnomenclature + # pypnusershub + # taxhub +pycparser==2.22 + # via cffi +pypn-ref-geo==1.5.4 + # via taxhub +pypnnomenclature @ https://github.com/PnX-SI/Nomenclature-api-module/archive/1.6.4.zip + # via -r requirements.ini +pypnusershub==3.0.3 + # via + # -r requirements.ini + # taxhub +python-dateutil==2.9.0.post0 + # via utils-flask-sqlalchemy +python-dotenv==1.1.0 + # via + # pypn-ref-geo + # pypnnomenclature + # taxhub +requests==2.32.3 + # via pypnusershub +shapely==2.1.1 + # via utils-flask-sqlalchemy-geo +six==1.17.0 + # via python-dateutil +sqlalchemy==1.4.54 + # via + # -r requirements.ini + # alembic + # flask-sqlalchemy + # geoalchemy2 + # marshmallow-sqlalchemy + # pypn-ref-geo + # pypnnomenclature + # pypnusershub + # taxhub + # utils-flask-sqlalchemy + # utils-flask-sqlalchemy-geo +taxhub==2.1.2 + # via pypnnomenclature +toml==0.10.2 + # via taxhub +typing-extensions==4.13.2 + # via alembic +urllib3==2.4.0 + # via + # requests + # taxhub +utils-flask-sqlalchemy==0.4.1 + # via + # pypn-ref-geo + # pypnnomenclature + # pypnusershub + # taxhub + # utils-flask-sqlalchemy-geo +utils-flask-sqlalchemy-geo==0.3.2 + # via pypn-ref-geo +werkzeug==3.1.3 + # via + # flask + # flask-cors + # flask-login +wtforms==3.1.2 + # via + # flask-admin + # taxhub +xmltodict==0.14.2 + # via pypnusershub diff --git a/backend/server.py b/backend/server.py index 983d2e3..ec84bc5 100644 --- a/backend/server.py +++ b/backend/server.py @@ -2,10 +2,11 @@ from flask import Flask, request, current_app from flask_sqlalchemy import SQLAlchemy from flask_cors import CORS +from pypnusershub.auth import auth_manager import datetime -from app.database import db +from flora_occitania.database import db db = db @@ -14,6 +15,14 @@ # import logging # logging.basicConfig() # logging.getLogger('sqlalchemy.engine').setLevel(logging.INFO) +providers_config = [ + # Default identity provider (comes with UH-AM) + { + "module": "pypnusershub.auth.providers.default.LocalProvider", + "id_provider": "local_provider", + }, + # you can add other identity providers that works with OpenID protocol (and many others !) +] def init_app(): if app_globals.get('app', False): @@ -21,6 +30,7 @@ def init_app(): else: app = Flask(__name__) + auth_manager.init_app(app, providers_declaration=providers_config) with app.app_context(): app.config.from_pyfile('config.py') @@ -31,7 +41,8 @@ def init_app(): from pypnnomenclature.routes import routes app.register_blueprint(routes, url_prefix="/nomenclatures") - from app.routes import adresses + from flora_occitania.routes import adresses + app.register_blueprint(adresses, url_prefix='') return app @@ -40,4 +51,4 @@ def init_app(): app = init_app() CORS(app, supports_credentials=True) if __name__ == '__main__': - app.run() \ No newline at end of file + app.run(port=1234) diff --git a/database.sql b/data/database.sql similarity index 59% rename from database.sql rename to data/database.sql index bedfba7..787ea14 100644 --- a/database.sql +++ b/data/database.sql @@ -16,35 +16,35 @@ WITH d AS ( SELECT * FROM ( VALUES - ('FO_LOCALISATION', 'CG', 'Causses gorges'), - ('FO_LOCALISATION', 'ML', 'Mont Lozère'), - ('FO_LOCALISATION', 'AI', 'Aigoual'), - ('FO_LOCALISATION', 'CV', 'Cévennes'), - ('FO_LOCALISATION', 'PG', 'Piémont garrigues'), - ('FO_LOCALISATION', 'IND', 'Indéterminé'), - ('FO_USAGE', 'ALIMENTAIRE', 'Alimentaire'), - ('FO_USAGE', 'MEDICINAL', 'Médicinal'), - ('FO_USAGE', 'VETERINAIRE', 'Vétérinaire'), - ('FO_USAGE', 'ALI_ANIMAL', 'Alimentation animale'), - ('FO_USAGE', 'LUDIQUE', 'Ludique'), - ('FO_USAGE', 'ARTISANAL', 'Artisanal'), - ('FO_USAGE', 'CROYANCES', 'Croyances et religions'), - ('FO_USAGE', 'TRADITION', 'Tradition orale'), - ('FO_PARTIE_PLANTE', 'PLANTE_ENTIERE', 'Plante entière'), - ('FO_PARTIE_PLANTE', 'TIGE', 'Tige'), - ('FO_PARTIE_PLANTE', 'RAMEAUX', 'Rameaux'), - ('FO_PARTIE_PLANTE', 'FEUILLES', 'Feuilles'), - ('FO_PARTIE_PLANTE', 'FLEURS', 'Fleurs'), - ('FO_PARTIE_PLANTE', 'BOURGEONS', 'Bourgeons'), - ('FO_PARTIE_PLANTE', 'FRUITS', 'Fruits'), - ('FO_PARTIE_PLANTE', 'RACINES', 'Racines'), - ('FO_PARTIE_PLANTE', 'BULBES', 'Bulbes'), - ('FO_PARTIE_PLANTE', 'RHIZOMES', 'Rhizomes'), - ('FO_PARTIE_PLANTE', 'SPORANGES', 'Sporanges'), - ('FO_PARTIE_PLANTE', 'NON_PRECISE', 'Non précisé') - ) as t(type, code, label) + ('FO_LOCALISATION', 'CG', 'CG', 'Causses gorges'), + ('FO_LOCALISATION', 'ML', 'ML', 'Mont Lozère'), + ('FO_LOCALISATION', 'AI', 'AI', 'Aigoual'), + ('FO_LOCALISATION', 'CV', 'CV', 'Cévennes'), + ('FO_LOCALISATION', 'PG', 'PG', 'Piémont garrigues'), + ('FO_LOCALISATION', 'IND', 'IND', 'Indéterminé'), + ('FO_USAGE', '1', 'ALIMENTAIRE', 'Alimentaire'), + ('FO_USAGE', '2', 'MEDICINAL', 'Médicinal'), + ('FO_USAGE', '3', 'VETERINAIRE', 'Vétérinaire'), + ('FO_USAGE', '4', 'ALI_ANIMAL', 'Alimentation animale'), + ('FO_USAGE', '5', 'LUDIQUE', 'Ludique'), + ('FO_USAGE', '6', 'ARTISANAL', 'Artisanal et domestique'), + ('FO_USAGE', '7', 'CROYANCES', 'Croyances et religions'), + ('FO_USAGE', '8', 'TRADITION', 'Tradition orale'), + ('FO_PARTIE_PLANTE', '1', 'PLANTE_ENTIERE', 'Plante entière'), + ('FO_PARTIE_PLANTE', '2', 'TIGE', 'Tige'), + ('FO_PARTIE_PLANTE', '3', 'RAMEAUX', 'Rameaux'), + ('FO_PARTIE_PLANTE', '4', 'FEUILLES', 'Feuilles'), + ('FO_PARTIE_PLANTE', '5', 'FLEURS', 'Fleurs'), + ('FO_PARTIE_PLANTE', '6', 'BOURGEONS', 'Bourgeons'), + ('FO_PARTIE_PLANTE', '7', 'FRUITS', 'Fruits'), + ('FO_PARTIE_PLANTE', '8', 'RACINES', 'Racines'), + ('FO_PARTIE_PLANTE', '9', 'BULBES', 'Bulbes'), + ('FO_PARTIE_PLANTE', '10', 'RHIZOMES', 'Rhizomes'), + ('FO_PARTIE_PLANTE', '11', 'SPORANGES', 'Sporanges'), + ('FO_PARTIE_PLANTE', '12', 'NON_PRECISE', 'Non précisé') + ) as t(type, code, mnemonique, label) ), a AS ( - SELECT b.id_type, d.code as cd_nomenclature, d.label as mnemonique, d.label as label_fr, d.label as label_default , + SELECT b.id_type, d.code as cd_nomenclature, d.mnemonique as mnemonique, d.label as label_fr, d.label as label_default , 'PNC' AS source, 'Validé' as statut, 0 as id_broader, b.id_type || '.' || lpad((row_number() OVER(PARTITION BY id_type))::varchar, 3, '0') as hierarchy, true as active @@ -216,12 +216,25 @@ CREATE OR REPLACE VIEW flora_occitania.v_list_summary_taxon_to_fill AS -- Données flore occitan --- ############################################################################################## --- ############################################################################################## -INSERT INTO flora_occitania.t_sources( citation, auteurs, titre, isbn) + + +INSERT INTO flora_occitania.t_sources(id_source, auteurs, titre, citation) VALUES - ('Vaissiera (Claudi), Botanica Occitana', 'Vaissiera (Claudi)', 'Botanica Occitana', NULL), - ('Renaux (Alain), Le Savoir en herbe', 'Renaux (Alain)', 'Le Savoir en herbe', NULL), - ('Wienin (Michel), Base de données FLORA- petite flore occitane des Garrigues et des Cévennes', 'Wienin (Michel)', 'petite flore occitane des Garrigues et des Cévennes', NULL), - ('Rodrigues Dos Santos (José), Savoirs de la nature, nature des savoirs', 'Rodrigues Dos Santos', 'Savoirs de la nature, nature des savoirs', NULL), - ('Ecologistes de l’Euzière : Les salades sauvages', 'Ecologistes de l’Euzière', 'Les salades sauvages', NULL), - ('Revue Cévennes 38-39, le Jardin des plantes', 'Parc national des Cévennes', 'le Jardin des plantes', NULL) +(1,'Alibert Louis', 'Dictionnaire occitan-français', 'Alibert Louis, Dictionnaire occitan-français, Toulouse, IEO,1966 – 1976.665 p.'), +(2,'Boissier de Sauvages Pierre-Augustin', 'Dictionnaire languedocien-français', 'Boissier de Sauvages Pierre-Augustin, Dictionnaire languedocien-français, Alais, 1820. T1, 389 p.'), +(3,'D''Hombres (Maximin) et Charvet (Gratien)', 'Dictionnaire languedocien-français', 'D''Hombres (Maximin) et Charvet (Gratien), Dictionnaire languedocien-français,Alais, 1870. T1, 313 p'), +(4,'Mistral Frédéric', 'Le Trésor du Félibrige', 'Mistral Frédéric, Le Trésor du Félibrige, Raphèle les Arles,1886. T1, 1196 p. T2, 1163.'), +(5,'Boucoiran (Louis)', 'Dictionnaire analogique et étymologique des idiomes méridionaux - Comprenant tous les termes vulgaires de la Flore et de la faune méridionale', 'Boucoiran (Louis), Dictionnaire analogique et étymologique des idiomes méridionaux - Comprenant tous les termes vulgaires de la Flore et de la faune méridionale, T1, 432 p.'), +(6,'Brisebarre (Anne Marie)', '« les plantes en suspension dans les bergeries cévenoles : efficacité symbolique ou phytothérapeutique ? »', 'Brisebarre (Anne Marie), « les plantes en suspension dans les bergeries cévenoles : efficacité symbolique ou phytothérapeutique ? » in : Plantes, sociétés, savoirs, symboles. Actes du séminaire d''ethnobotanique de Salagon, vol.3, années 2003-2004. p 127- 136.'), +(7,'Durand-Tullou (Adrienne)', '« Rôle des végétaux dans le mode de vie traditionnel »', 'Durand-Tullou (Adrienne), « Rôle des végétaux dans le mode de vie traditionnel » in : L'' Encyclopédie des Cévennes, l''almanach Cévenol, n°8. '), +(8,'Ecologistes de l’Euzière', 'Les salades sauvages', 'Ecologistes de l’Euzière, Les salades sauvages, l''ensalada champanèla. Nîmes, Editions Ecologistes de l''Euzière, 2011. 176 p.'), +(9,'Laurence Pierre', 'Du paysage et des temps, la mémoire orale en Cévennes Vallée française et pays de Calberte. Parc national des Cévennes', 'Laurence Pierre, Du paysage et des temps, la mémoire orale en Cévennes Vallée française et pays de Calberte. Parc national des Cévennes, 2004. T1431 p , T2 860p.'), +(10,'Le Jardin des plantes', 'Revue du Parc national des Cévennes n°38-39', 'Le Jardin des plantes, Revue du Parc national des Cévennes n°38-39, 1988.'), +(11,'Renaux (Alain)', 'Le Savoir en herbe. Montpellier : Presse du Languedoc', 'Renaux (Alain), Le Savoir en herbe. Montpellier : Presse du Languedoc,1998. 426 p.'), +(12,'Seignolles Claude', 'Le folklore du Languedoc -Gard', 'Seignolles Claude, Le folklore du Languedoc -Gard, Hérault, Lozère- . Paris : Maisonneuve et Larose, 1977. 302 p.'), +(13,'Rodrigues Dos Santos (José)', 'Savoirs de la nature, nature des savoirs : les savoirs de la flore en Cévennes.', 'Rodrigues Dos Santos (José), Savoirs de la nature, nature des savoirs : les savoirs de la flore en Cévennes. Paris, ANRT,1995. 698 p.'), +(14,'Vaissiera (Claudi)', 'Botanica Occitana', 'Vaissiera (Claudi), Botanica Occitana. Béziers : Institut d’estudis occitans, 1989. T1,166 p, T2, 241 p.'), +(15,'Wienin Michel', 'petite flore occitane des Garrigues et des Cévennes', 'Wienin Michel, petite flore occitane des Garrigues et des Cévennes. (notice 18 p).') ; + + diff --git a/data/migrations/0.0.1to0.0.2.sql b/data/migrations/0.0.1to0.0.2.sql new file mode 100644 index 0000000..2e461a9 --- /dev/null +++ b/data/migrations/0.0.1to0.0.2.sql @@ -0,0 +1,39 @@ + +UPDATE ref_nomenclatures.t_nomenclatures n SET mnemonique = a.cd_nomenclature, cd_nomenclature = rank +FROM ( +SELECT rank() OVER(ORDER BY hierarchy), * FROM ref_nomenclatures.t_nomenclatures +WHERE id_type = + ref_nomenclatures.get_id_nomenclature_type('FO_USAGE') +) a +WHERE a.id_nomenclature = n.id_nomenclature; + + +UPDATE flora_occitania.t_sources SET id_source = id_source +100; + +INSERT INTO flora_occitania.t_sources(id_source, auteurs, titre, citation) +VALUES +(1,'Alibert Louis', 'Dictionnaire occitan-français', 'Alibert Louis, Dictionnaire occitan-français, Toulouse, IEO,1966 – 1976.665 p.'), +(2,'Boissier de Sauvages Pierre-Augustin', 'Dictionnaire languedocien-français', 'Boissier de Sauvages Pierre-Augustin, Dictionnaire languedocien-français, Alais, 1820. T1, 389 p.'), +(3,'D''Hombres (Maximin) et Charvet (Gratien)', 'Dictionnaire languedocien-français', 'D''Hombres (Maximin) et Charvet (Gratien), Dictionnaire languedocien-français,Alais, 1870. T1, 313 p'), +(4,'Mistral Frédéric', 'Le Trésor du Félibrige', 'Mistral Frédéric, Le Trésor du Félibrige, Raphèle les Arles,1886. T1, 1196 p. T2, 1163.'), +(5,'Boucoiran (Louis)', 'Dictionnaire analogique et étymologique des idiomes méridionaux - Comprenant tous les termes vulgaires de la Flore et de la faune méridionale', 'Boucoiran (Louis), Dictionnaire analogique et étymologique des idiomes méridionaux - Comprenant tous les termes vulgaires de la Flore et de la faune méridionale, T1, 432 p.'), +(6,'Brisebarre (Anne Marie)', '« les plantes en suspension dans les bergeries cévenoles : efficacité symbolique ou phytothérapeutique ? »', 'Brisebarre (Anne Marie), « les plantes en suspension dans les bergeries cévenoles : efficacité symbolique ou phytothérapeutique ? » in : Plantes, sociétés, savoirs, symboles. Actes du séminaire d''ethnobotanique de Salagon, vol.3, années 2003-2004. p 127- 136.'), +(7,'Durand-Tullou (Adrienne)', '« Rôle des végétaux dans le mode de vie traditionnel »', 'Durand-Tullou (Adrienne), « Rôle des végétaux dans le mode de vie traditionnel » in : L'' Encyclopédie des Cévennes, l''almanach Cévenol, n°8. '), +(8,'Ecologistes de l’Euzière', 'Les salades sauvages', 'Ecologistes de l’Euzière, Les salades sauvages, l''ensalada champanèla. Nîmes, Editions Ecologistes de l''Euzière, 2011. 176 p.'), +(9,'Laurence Pierre', 'Du paysage et des temps, la mémoire orale en Cévennes Vallée française et pays de Calberte. Parc national des Cévennes', 'Laurence Pierre, Du paysage et des temps, la mémoire orale en Cévennes Vallée française et pays de Calberte. Parc national des Cévennes, 2004. T1431 p , T2 860p.'), +(10,'Le Jardin des plantes', 'Revue du Parc national des Cévennes n°38-39', 'Le Jardin des plantes, Revue du Parc national des Cévennes n°38-39, 1988.'), +(11,'Renaux (Alain)', 'Le Savoir en herbe. Montpellier : Presse du Languedoc', 'Renaux (Alain), Le Savoir en herbe. Montpellier : Presse du Languedoc,1998. 426 p.'), +(12,'Seignolles Claude', 'Le folklore du Languedoc -Gard', 'Seignolles Claude, Le folklore du Languedoc -Gard, Hérault, Lozère- . Paris : Maisonneuve et Larose, 1977. 302 p.'), +(13,'Rodrigues Dos Santos (José)', 'Savoirs de la nature, nature des savoirs : les savoirs de la flore en Cévennes.', 'Rodrigues Dos Santos (José), Savoirs de la nature, nature des savoirs : les savoirs de la flore en Cévennes. Paris, ANRT,1995. 698 p.'), +(14,'Vaissiera (Claudi)', 'Botanica Occitana', 'Vaissiera (Claudi), Botanica Occitana. Béziers : Institut d’estudis occitans, 1989. T1,166 p, T2, 241 p.'), +(15,'Wienin Michel', 'petite flore occitane des Garrigues et des Cévennes', 'Wienin Michel, petite flore occitane des Garrigues et des Cévennes. (notice 18 p).') +; + +UPDATE flora_occitania.t_nom_vernaculaires SET id_sources = array_replace(id_sources, '1', '14'); +UPDATE flora_occitania.t_nom_vernaculaires SET id_sources = array_replace(id_sources, '2', '11'); +UPDATE flora_occitania.t_nom_vernaculaires SET id_sources = array_replace(id_sources, '3', '15'); +UPDATE flora_occitania.t_nom_vernaculaires SET id_sources = array_replace(id_sources, '4', '13'); +UPDATE flora_occitania.t_nom_vernaculaires SET id_sources = array_replace(id_sources, '5', '8'); +UPDATE flora_occitania.t_nom_vernaculaires SET id_sources = array_replace(id_sources, '6', '10'); + +DELETE FROM flora_occitania.t_sources WHERE id_source > 100; \ No newline at end of file diff --git a/frontend/.nvmrc b/frontend/.nvmrc new file mode 100644 index 0000000..cc5875f --- /dev/null +++ b/frontend/.nvmrc @@ -0,0 +1 @@ +v10.15.3 diff --git a/frontend/flora-occitania/package-lock.json b/frontend/flora-occitania/package-lock.json index 19308d4..7a605d5 100644 --- a/frontend/flora-occitania/package-lock.json +++ b/frontend/flora-occitania/package-lock.json @@ -2316,8 +2316,7 @@ "abbrev": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", - "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", - "optional": true + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" }, "accepts": { "version": "1.3.7", @@ -2386,21 +2385,6 @@ "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-7.0.0.tgz", "integrity": "sha512-7Bv1We7ZGuU79zZbb6rRqcpxo3OY+zrdtloZWoyD8fmGX+FeXRjE+iuGkZjSXLVovLzrsvMGMy0EkwA0E0umxg==" }, - "acorn5-object-spread": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/acorn5-object-spread/-/acorn5-object-spread-4.0.0.tgz", - "integrity": "sha1-1XWAge7ZcSGrC+R+Mcqu8qo5lpc=", - "requires": { - "acorn": "^5.1.2" - }, - "dependencies": { - "acorn": { - "version": "5.7.3", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.7.3.tgz", - "integrity": "sha512-T/zvzYRfbVojPWahDsE5evJdHb3oJoQfFbsrKM7w5Zcs++Tr257tia3BmMP8XYVjp1S9RZXQMh7gao96BlqZOw==" - } - } - }, "adm-zip": { "version": "0.4.13", "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.4.13.tgz", @@ -9058,6 +9042,11 @@ "integrity": "sha512-iyam8fBuCUpWeKPGpaNMetEocMt364qkCsfL9JuhjXX6dRnguRVOfk2GZaDpPjcOKiiXCPINZC1GczQ7iTq3Zw==", "dev": true }, + "ng2-cookies": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/ng2-cookies/-/ng2-cookies-1.0.12.tgz", + "integrity": "sha1-Pz5hPgE3sGSbcFxngHS0vQgUnMw=" + }, "ngrok": { "version": "3.2.5", "resolved": "https://registry.npmjs.org/ngrok/-/ngrok-3.2.5.tgz", @@ -11129,8 +11118,7 @@ }, "ansi-regex": { "version": "2.1.1", - "bundled": true, - "optional": true + "bundled": true }, "aproba": { "version": "1.2.0", @@ -11167,8 +11155,7 @@ }, "code-point-at": { "version": "1.1.0", - "bundled": true, - "optional": true + "bundled": true }, "concat-map": { "version": "0.0.1", @@ -11177,8 +11164,7 @@ }, "console-control-strings": { "version": "1.1.0", - "bundled": true, - "optional": true + "bundled": true }, "core-util-is": { "version": "1.0.2", @@ -11292,7 +11278,6 @@ "is-fullwidth-code-point": { "version": "1.0.0", "bundled": true, - "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -11318,7 +11303,6 @@ "minipass": { "version": "2.3.5", "bundled": true, - "optional": true, "requires": { "safe-buffer": "^5.1.2", "yallist": "^3.0.0" @@ -11335,7 +11319,6 @@ "mkdirp": { "version": "0.5.1", "bundled": true, - "optional": true, "requires": { "minimist": "0.0.8" } @@ -11408,8 +11391,7 @@ }, "number-is-nan": { "version": "1.0.1", - "bundled": true, - "optional": true + "bundled": true }, "object-assign": { "version": "4.1.1", @@ -11419,7 +11401,6 @@ "once": { "version": "1.4.0", "bundled": true, - "optional": true, "requires": { "wrappy": "1" } @@ -11495,8 +11476,7 @@ }, "safe-buffer": { "version": "5.1.2", - "bundled": true, - "optional": true + "bundled": true }, "safer-buffer": { "version": "2.1.2", @@ -11526,7 +11506,6 @@ "string-width": { "version": "1.0.2", "bundled": true, - "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -11544,7 +11523,6 @@ "strip-ansi": { "version": "3.0.1", "bundled": true, - "optional": true, "requires": { "ansi-regex": "^2.0.0" } @@ -11583,13 +11561,11 @@ }, "wrappy": { "version": "1.0.2", - "bundled": true, - "optional": true + "bundled": true }, "yallist": { "version": "3.0.3", - "bundled": true, - "optional": true + "bundled": true } } }, @@ -11952,7 +11928,6 @@ "resolved": "https://registry.npmjs.org/rijs.sync/-/rijs.sync-2.3.5.tgz", "integrity": "sha512-tcbhmjLyWb+2s2gdiSmROEoD/OQPFeKC9xBnKgs0H+umY8CaVrVPGFdr1y1qovm7HxUbdk/BKqi94GQDc5XB3A==", "requires": { - "buble": "github:pemrouz/buble", "express": "^4.14.0", "lru_map": "^0.3.3", "platform": "^1.3.4", @@ -11961,13 +11936,13 @@ }, "dependencies": { "acorn": { - "version": "5.7.3", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.7.3.tgz", - "integrity": "sha512-T/zvzYRfbVojPWahDsE5evJdHb3oJoQfFbsrKM7w5Zcs++Tr257tia3BmMP8XYVjp1S9RZXQMh7gao96BlqZOw==" + "version": "5.7.4", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.7.4.tgz", + "integrity": "sha512-1D++VG7BhrtvQpNbBzovKNc1FLGGEE/oGe7b9xJm/RFHMBeUaUGpluV9RLjZa47YFdPcDAenEYuq9pQPcMdLJg==" }, "buble": { "version": "github:pemrouz/buble#4e639aeeb64712ac95dc30a52750d1ee4432c9c8", - "from": "github:pemrouz/buble", + "from": "github:pemrouz/buble#4e639aeeb64712ac95dc30a52750d1ee4432c9c8", "requires": { "acorn": "^5.1.2", "acorn-jsx": "^3.0.1", @@ -11977,6 +11952,16 @@ "minimist": "^1.2.0", "os-homedir": "^1.0.1", "vlq": "^0.2.2" + }, + "dependencies": { + "acorn5-object-spread": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/acorn5-object-spread/-/acorn5-object-spread-4.0.0.tgz", + "integrity": "sha512-l+UYpDk+mjQoTXUHtSyUUb6glz9sSnl283LYLKvZJ7Nxpn4taIdP6DAr+8GwQ8UyY95tLWoKIr4/P7OWcw6WWw==", + "requires": { + "acorn": "^5.1.2" + } + } } }, "magic-string": { @@ -14032,8 +14017,7 @@ "ansi-regex": { "version": "2.1.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "aproba": { "version": "1.2.0", @@ -14076,8 +14060,7 @@ "code-point-at": { "version": "1.1.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "concat-map": { "version": "0.0.1", @@ -14088,8 +14071,7 @@ "console-control-strings": { "version": "1.1.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "core-util-is": { "version": "1.0.2", @@ -14206,8 +14188,7 @@ "inherits": { "version": "2.0.3", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "ini": { "version": "1.3.5", @@ -14219,7 +14200,6 @@ "version": "1.0.0", "bundled": true, "dev": true, - "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -14249,7 +14229,6 @@ "version": "2.3.5", "bundled": true, "dev": true, - "optional": true, "requires": { "safe-buffer": "^5.1.2", "yallist": "^3.0.0" @@ -14268,7 +14247,6 @@ "version": "0.5.1", "bundled": true, "dev": true, - "optional": true, "requires": { "minimist": "0.0.8" } @@ -14362,7 +14340,6 @@ "version": "1.4.0", "bundled": true, "dev": true, - "optional": true, "requires": { "wrappy": "1" } @@ -14448,8 +14425,7 @@ "safe-buffer": { "version": "5.1.2", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "safer-buffer": { "version": "2.1.2", @@ -14485,7 +14461,6 @@ "version": "1.0.2", "bundled": true, "dev": true, - "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -14505,7 +14480,6 @@ "version": "3.0.1", "bundled": true, "dev": true, - "optional": true, "requires": { "ansi-regex": "^2.0.0" } @@ -14549,14 +14523,12 @@ "wrappy": { "version": "1.0.2", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "yallist": { "version": "3.0.3", "bundled": true, - "dev": true, - "optional": true + "dev": true } } }, @@ -14875,8 +14847,7 @@ "ansi-regex": { "version": "2.1.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "aproba": { "version": "1.2.0", @@ -14919,8 +14890,7 @@ "code-point-at": { "version": "1.1.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "concat-map": { "version": "0.0.1", @@ -14931,8 +14901,7 @@ "console-control-strings": { "version": "1.1.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "core-util-is": { "version": "1.0.2", @@ -15049,8 +15018,7 @@ "inherits": { "version": "2.0.3", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "ini": { "version": "1.3.5", @@ -15062,7 +15030,6 @@ "version": "1.0.0", "bundled": true, "dev": true, - "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -15085,14 +15052,12 @@ "minimist": { "version": "0.0.8", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "minipass": { "version": "2.3.5", "bundled": true, "dev": true, - "optional": true, "requires": { "safe-buffer": "^5.1.2", "yallist": "^3.0.0" @@ -15111,7 +15076,6 @@ "version": "0.5.1", "bundled": true, "dev": true, - "optional": true, "requires": { "minimist": "0.0.8" } @@ -15192,8 +15156,7 @@ "number-is-nan": { "version": "1.0.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "object-assign": { "version": "4.1.1", @@ -15205,7 +15168,6 @@ "version": "1.4.0", "bundled": true, "dev": true, - "optional": true, "requires": { "wrappy": "1" } @@ -15291,8 +15253,7 @@ "safe-buffer": { "version": "5.1.2", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "safer-buffer": { "version": "2.1.2", @@ -15328,7 +15289,6 @@ "version": "1.0.2", "bundled": true, "dev": true, - "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -15348,7 +15308,6 @@ "version": "3.0.1", "bundled": true, "dev": true, - "optional": true, "requires": { "ansi-regex": "^2.0.0" } @@ -15392,14 +15351,12 @@ "wrappy": { "version": "1.0.2", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "yallist": { "version": "3.0.3", "bundled": true, - "dev": true, - "optional": true + "dev": true } } }, diff --git a/frontend/flora-occitania/package.json b/frontend/flora-occitania/package.json index 3ade101..813be36 100644 --- a/frontend/flora-occitania/package.json +++ b/frontend/flora-occitania/package.json @@ -26,6 +26,8 @@ "datatables.net-dt": "^1.10.19", "font-awesome": "^4.7.0", "jquery": "^3.4.1", + "lodash": "^4.17.15", + "ng2-cookies": "^1.0.12", "popper": "^1.0.1", "popper.js": "^1.15.0", "rxjs": "~6.4.0", diff --git a/frontend/flora-occitania/src/app/app-routing.module.ts b/frontend/flora-occitania/src/app/app-routing.module.ts index 47a8a61..45b80ef 100644 --- a/frontend/flora-occitania/src/app/app-routing.module.ts +++ b/frontend/flora-occitania/src/app/app-routing.module.ts @@ -4,10 +4,12 @@ import { Routes, RouterModule } from '@angular/router'; import {TableComponent} from './dashboard/table/table.component'; import {TaxonDetailComponent} from './taxon-detail/taxon-detail.component'; import {FormEthnobotaComponent} from './form/form-ethnobota/form-ethnobota.component'; +import {LoginComponent} from './login/login.component'; const routes: Routes = [ { path: '', redirectTo: '/dashboard', pathMatch: 'full' }, { path: 'dashboard', component: TableComponent }, + { path: 'login', component: LoginComponent }, { path: 'detail/:id', component: TaxonDetailComponent }, { path: 'form/:id', component: FormEthnobotaComponent } ]; diff --git a/frontend/flora-occitania/src/app/app.component.html b/frontend/flora-occitania/src/app/app.component.html index 60e908c..a0ea147 100644 --- a/frontend/flora-occitania/src/app/app.component.html +++ b/frontend/flora-occitania/src/app/app.component.html @@ -8,6 +8,12 @@ Dashboard (current) +
diff --git a/frontend/flora-occitania/src/app/app.component.ts b/frontend/flora-occitania/src/app/app.component.ts index 5710162..fccebf3 100644 --- a/frontend/flora-occitania/src/app/app.component.ts +++ b/frontend/flora-occitania/src/app/app.component.ts @@ -1,5 +1,9 @@ import { Component } from '@angular/core'; +import { Router } from '@angular/router'; + +import { AuthenticationService, User } from './services/authentication.service'; + @Component({ selector: 'app-root', templateUrl: './app.component.html', @@ -7,4 +11,19 @@ import { Component } from '@angular/core'; }) export class AppComponent { title = 'flora-occitania'; -} + currentUser: User; + + constructor( + private router: Router, + private authenticationService: AuthenticationService + ) { + this.authenticationService.currentUser.subscribe( + x => this.currentUser = x + ); + } + + logout() { + this.authenticationService.logout(); + this.router.navigate(['/']); + } +} \ No newline at end of file diff --git a/frontend/flora-occitania/src/app/app.module.ts b/frontend/flora-occitania/src/app/app.module.ts index 51301dd..d5c2067 100644 --- a/frontend/flora-occitania/src/app/app.module.ts +++ b/frontend/flora-occitania/src/app/app.module.ts @@ -1,11 +1,18 @@ import { BrowserModule } from '@angular/platform-browser'; import { NgModule , APP_INITIALIZER } from '@angular/core'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; -import { HttpClient, HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http'; +import { + HttpClient, HttpRequest, HttpHandler, HttpEvent , HttpClientModule , HttpInterceptor, HTTP_INTERCEPTORS +} from '@angular/common/http'; + +import { Observable } from 'rxjs'; +import { Injectable } from '@angular/core'; import { forkJoin } from 'rxjs'; import { DataTablesModule } from 'angular-datatables'; +import { CookieService } from 'ng2-cookies'; + import { AppRoutingModule } from './app-routing.module'; import { AppComponent } from './app.component'; import { TableComponent } from './dashboard/table/table.component'; @@ -17,6 +24,7 @@ import { MultiselectComponent } from './form/generic-form/multiselect/multiselec import {NomenclatureService} from './services/nomenclature.service'; import {FloraOccitaniaService} from './services/flora-occitania.service'; import { DisplayForeignkeyArrayComponent } from './taxon-detail/display-foreignkey-array/display-foreignkey-array.component'; +import { LoginComponent } from './login/login.component'; export function initApp( nomeclatureService: NomenclatureService, @@ -32,6 +40,17 @@ export function initApp( }); }; } +@Injectable() +export class AddCredentialsInterceptor implements HttpInterceptor { + constructor() {} + + intercept(request: HttpRequest, next: HttpHandler): Observable> { + request = request.clone({ + withCredentials: true + }); + return next.handle(request); + } +} @NgModule({ @@ -41,7 +60,8 @@ export function initApp( TaxonDetailComponent, FormEthnobotaComponent, MultiselectComponent, - DisplayForeignkeyArrayComponent + DisplayForeignkeyArrayComponent, + LoginComponent ], imports: [ BrowserModule, @@ -52,12 +72,18 @@ export function initApp( DataTablesModule ], providers: [ + + CookieService, { provide: APP_INITIALIZER, useFactory: initApp, multi: true, deps: [NomenclatureService, FloraOccitaniaService] - } + }, { + provide: HTTP_INTERCEPTORS, + useClass: AddCredentialsInterceptor, + multi: true, + } ], bootstrap: [AppComponent] }) diff --git a/frontend/flora-occitania/src/app/appSettings.ts.sample b/frontend/flora-occitania/src/app/appSettings.ts.sample index 7171403..ce9d7c0 100644 --- a/frontend/flora-occitania/src/app/appSettings.ts.sample +++ b/frontend/flora-occitania/src/app/appSettings.ts.sample @@ -2,4 +2,5 @@ export class AppSettings { public static API_ENDPOINT = 'http://127.0.0.1:1234/'; public static TAXHUB_ENDPOINT = 'http://127.0.0.1:5000/api/'; public static ID_LIST = 68; + public static ID_APPLICATION = 53; } \ No newline at end of file diff --git a/frontend/flora-occitania/src/app/form/form-ethnobota/form-ethnobota.component.html b/frontend/flora-occitania/src/app/form/form-ethnobota/form-ethnobota.component.html index 3562f14..f614f61 100644 --- a/frontend/flora-occitania/src/app/form/form-ethnobota/form-ethnobota.component.html +++ b/frontend/flora-occitania/src/app/form/form-ethnobota/form-ethnobota.component.html @@ -47,7 +47,7 @@

{{taxon.nom_complet}}

{{taxon.nom_complet}} {{taxon.nom_complet}} {{taxon.nom_complet}} {{taxon.nom_complet}} +
diff --git a/frontend/flora-occitania/src/app/form/form-ethnobota/form-ethnobota.component.ts b/frontend/flora-occitania/src/app/form/form-ethnobota/form-ethnobota.component.ts index 01917ac..1e47e3d 100644 --- a/frontend/flora-occitania/src/app/form/form-ethnobota/form-ethnobota.component.ts +++ b/frontend/flora-occitania/src/app/form/form-ethnobota/form-ethnobota.component.ts @@ -1,14 +1,13 @@ import { Component, OnInit, Output, EventEmitter } from '@angular/core'; -import {FormArray, FormBuilder, FormControl, FormGroup, Validators, AbstractControl} from "@angular/forms"; +import {FormArray, FormBuilder, FormControl, FormGroup, Validators, AbstractControl} from '@angular/forms'; import { ActivatedRoute, Router } from '@angular/router'; import { Location } from '@angular/common'; -import {FloraOccitaniaService, TaxonList} from "../../services/flora-occitania.service"; -import {NomenclatureService} from "../../services/nomenclature.service"; -import {TaxhubService} from "../../services/taxhub.service"; - +import {FloraOccitaniaService, TaxonList} from '../../services/flora-occitania.service'; +import {NomenclatureService} from '../../services/nomenclature.service'; +import {TaxhubService} from '../../services/taxhub.service'; @Component({ selector: 'app-form-ethnobota', @@ -19,7 +18,8 @@ export class FormEthnobotaComponent implements OnInit { taxon: any; sources: Array; - + // Nom des popriétés à exclure lors de la duplucation de formulaire + excludeValues: Array = ['nom_vernaculaire', 'commentaire_nom']; nomVernForm = this.formBuilder.group({ commentaire_general: [''], nomVerns: this.formBuilder.array([]) @@ -37,31 +37,33 @@ export class FormEthnobotaComponent implements OnInit { ngOnInit() { this.getTaxon(); - console.log(this.floraOccitaniaService.sources); } + getNomVernForm(): FormArray { + return this.nomVernForm.controls.nomVerns as FormArray; + } getTaxon(): void { const id = +this.route.snapshot.paramMap.get('id'); this.floraOccitaniaService.getTaxonDetail(id).subscribe( data => { this.taxon = data.items[0]; - const control = this.nomVernForm.controls.nomVerns; + const control = this.getNomVernForm(); this.nomVernForm.controls.commentaire_general.setValue( - this.taxon["commentaire_general"] + this.taxon.commentaire_general ); - if ('noms_occitan' in this.taxon){ - this.taxon["noms_occitan"].forEach(x => { + if ('noms_occitan' in this.taxon) { + this.taxon.noms_occitan.forEach(x => { const ctl = this.getNewNomVern(); ctl.patchValue(x); control.push(ctl); }); } } - ) + ); } - getNewNomVern(): FormGroup{ + getNewNomVern(): FormGroup { return this.formBuilder.group({ nom_vernaculaire: ['', Validators.required], commentaire_nom: [''], @@ -74,20 +76,53 @@ export class FormEthnobotaComponent implements OnInit { }); } - addNewNomVern():void{ + addNewNomVern(): void { if (this.taxon) { - let control = this.nomVernForm.controls.nomVerns; + const control = this.getNomVernForm(); let newNomVern = this.getNewNomVern(); - newNomVern.controls.cd_ref.setValue(this.taxon.cd_ref); + newNomVern = this.populateNomVernForm( + newNomVern, + {cd_ref: this.taxon.cd_ref} + ); control.push(newNomVern); } else { - console.log("Taxon non connu"); + console.log('Taxon non connu'); } } + populateNomVernForm(form: FormGroup, values: {}): FormGroup { + Object.entries(values).forEach( + ([key, val]) => { + form.controls[key].setValue(val); + } + ); + return form; + } + deleteNomVern(index): void { - let control = this.nomVernForm.controls.nomVerns; - control.removeAt(index) + const control = this.getNomVernForm(); + control.removeAt(index); + } + + duplicateNomVern(index): void { + const control = this.getNomVernForm(); + + const toClone = control.at(index) as FormGroup; + let newNomVern = this.getNewNomVern(); + + const values = {}; + + Object.keys(toClone.controls).forEach(key => { + if ( ! this.excludeValues.includes(key) ) { + values[key] = toClone.controls[key].value; + } + }); + + newNomVern = this.populateNomVernForm( + newNomVern, + values + ); + control.push(newNomVern); } submit(): void { @@ -98,7 +133,6 @@ export class FormEthnobotaComponent implements OnInit { ).subscribe( data => { this.router.navigate([`/detail/${this.taxon.id_nom}`]); - console.log(data); } ); } diff --git a/frontend/flora-occitania/src/app/form/generic-form/multiselect/multiselect.component.html b/frontend/flora-occitania/src/app/form/generic-form/multiselect/multiselect.component.html index c0afb64..88c39f7 100644 --- a/frontend/flora-occitania/src/app/form/generic-form/multiselect/multiselect.component.html +++ b/frontend/flora-occitania/src/app/form/generic-form/multiselect/multiselect.component.html @@ -5,7 +5,8 @@ aria-expanded="true" style="white-space: normal;" id="button-input"> - {{item[keyLabel]}} + {{item['displayValue']}}
@@ -15,7 +16,9 @@ diff --git a/frontend/flora-occitania/src/app/form/generic-form/multiselect/multiselect.component.ts b/frontend/flora-occitania/src/app/form/generic-form/multiselect/multiselect.component.ts index 3a569b5..f7bca56 100644 --- a/frontend/flora-occitania/src/app/form/generic-form/multiselect/multiselect.component.ts +++ b/frontend/flora-occitania/src/app/form/generic-form/multiselect/multiselect.component.ts @@ -1,6 +1,6 @@ import { Component, OnInit, Input, EventEmitter, Output, OnChanges } from '@angular/core'; import { FormControl } from '@angular/forms'; -import {debounceTime, distinctUntilChanged} from "rxjs/operators"; + @Component({ selector: 'form-multiselect', templateUrl: './multiselect.component.html', @@ -12,12 +12,12 @@ export class MultiselectComponent implements OnInit { public formControlValue = []; public savedValues = []; @Input() parentFormControl: FormControl; - //** Valeurs à afficher dans la liste déroulante. Doit être un tableau de dictionnaire */ - @Input() values:Array; + // ** Valeurs à afficher dans la liste déroulante. Doit être un tableau de dictionnaire */ + @Input() values: Array; /** * Clé du dictionnaire de valeur que le composant doit prendre pour l'affichage de la liste déroulante */ - @Input() keyLabel: string; + @Input() keyLabel: Array; /** Clé du dictionnaire que le composant doit passer au formControl */ @Input() keyValue: string; /** Est-ce que le composant doit afficher l'item "tous" dans les options du select ? */ @@ -32,10 +32,12 @@ export class MultiselectComponent implements OnInit { // time before the output are triggered @Input() debounceTime: number; + @Output() onSearch = new EventEmitter(); @Output() onChange = new EventEmitter(); @Output() onDelete = new EventEmitter(); + constructor() {} // Component to generate a custom multiselect input with a search bar (which can be disabled) @@ -48,13 +50,34 @@ export class MultiselectComponent implements OnInit { this.displayAll = this.displayAll || false; + this.values.forEach((value, i, array) => { + const displayValue = []; + Object.keys(value).map( key => { + if (this.keyLabel.includes(key)) { + displayValue[this.keyLabel.indexOf(key)] = value[key]; + } + }); + displayValue.filter(v => {if (v) { return v; }}); + + this.values[i]['displayValue'] = displayValue.join('-'); + }); + + + // sort by value + // TODO enable customisation by user + this.values.sort((a, b) => { + return a.displayValue.localeCompare(b.displayValue); + }); + if (this.values && this.parentFormControl.value) { this.values.forEach(value => { if (this.parentFormControl.value.indexOf(value[this.keyValue]) !== -1) { this.selectedItems.push(value); + this.formControlValue.push(value[this.keyValue]); } }); } + // remove doublon in the dropdown lists this.removeDoublon(); @@ -78,7 +101,7 @@ export class MultiselectComponent implements OnInit { addItem(item) { // remove element from the items list to avoid doublon this.values = this.values.filter(curItem => { - return curItem[this.keyLabel] !== item[this.keyLabel]; + return curItem['displayValue'] !== item['displayValue']; }); // set the item for the formControl @@ -96,7 +119,7 @@ export class MultiselectComponent implements OnInit { removeItem($event, item) { // remove element from the items list to avoid doublon this.values = this.values.filter(curItem => { - return curItem[this.keyLabel] !== item[this.keyLabel]; + return curItem['displayValue'] !== item['displayValue']; }); // disable event propagation $event.stopPropagation(); @@ -104,7 +127,7 @@ export class MultiselectComponent implements OnInit { this.values.push(item); this.selectedItems = this.selectedItems.filter(curItem => { - return curItem[this.keyLabel] !== item[this.keyLabel]; + return curItem['displayValue'] !== item['displayValue']; }); this.formControlValue = this.formControlValue.filter(el => { diff --git a/frontend/flora-occitania/src/app/login/login.component.css b/frontend/flora-occitania/src/app/login/login.component.css new file mode 100644 index 0000000..e69de29 diff --git a/frontend/flora-occitania/src/app/login/login.component.html b/frontend/flora-occitania/src/app/login/login.component.html new file mode 100644 index 0000000..40cf4fc --- /dev/null +++ b/frontend/flora-occitania/src/app/login/login.component.html @@ -0,0 +1,24 @@ +

Login

+
+ +
+ + +
+
Username is required
+
+
+
+ + +
+
Password is required
+
+
+
+ + +
+
\ No newline at end of file diff --git a/frontend/flora-occitania/src/app/login/login.component.spec.ts b/frontend/flora-occitania/src/app/login/login.component.spec.ts new file mode 100644 index 0000000..d6d85a8 --- /dev/null +++ b/frontend/flora-occitania/src/app/login/login.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { LoginComponent } from './login.component'; + +describe('LoginComponent', () => { + let component: LoginComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ LoginComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(LoginComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/frontend/flora-occitania/src/app/login/login.component.ts b/frontend/flora-occitania/src/app/login/login.component.ts new file mode 100644 index 0000000..b9bc04c --- /dev/null +++ b/frontend/flora-occitania/src/app/login/login.component.ts @@ -0,0 +1,63 @@ + +import { Component, OnInit } from '@angular/core'; +import { Router, ActivatedRoute } from '@angular/router'; +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { first } from 'rxjs/operators'; + +import { AuthenticationService } from '../services/authentication.service'; + +@Component({ + selector: 'app-login', + templateUrl: './login.component.html', + styleUrls: ['./login.component.css'] +}) +export class LoginComponent implements OnInit { + loginForm: FormGroup; + loading = false; + submitted = false; + returnUrl: string; + authentErrors; + + constructor( + private formBuilder: FormBuilder, + private route: ActivatedRoute, + private router: Router, + private authenticationService: AuthenticationService + ) { + + } + + ngOnInit() { + this.loginForm = this.formBuilder.group({ + username: ['', Validators.required], + password: ['', Validators.required] + }); + + // get return url from route parameters or default to '/' + this.returnUrl = this.route.snapshot.queryParams['returnUrl'] || '/'; + } + + // convenience getter for easy access to form fields + get f() { return this.loginForm.controls; } + + onSubmit() { + this.submitted = true; + + // stop here if form is invalid + if (this.loginForm.invalid) { + return; + } + + this.loading = true; + this.authenticationService.login(this.f.username.value, this.f.password.value) + .pipe(first()) + .subscribe( + data => { + this.router.navigate([this.returnUrl]); + }, + error => { + this.authentErrors = error.error.msg; + this.loading = false; + }); + } +} \ No newline at end of file diff --git a/frontend/flora-occitania/src/app/services/authentication.service.spec.ts b/frontend/flora-occitania/src/app/services/authentication.service.spec.ts new file mode 100644 index 0000000..91a1e97 --- /dev/null +++ b/frontend/flora-occitania/src/app/services/authentication.service.spec.ts @@ -0,0 +1,12 @@ +import { TestBed } from '@angular/core/testing'; + +import { AuthenticationService } from './authentication.service'; + +describe('AuthenticationService', () => { + beforeEach(() => TestBed.configureTestingModule({})); + + it('should be created', () => { + const service: AuthenticationService = TestBed.get(AuthenticationService); + expect(service).toBeTruthy(); + }); +}); diff --git a/frontend/flora-occitania/src/app/services/authentication.service.ts b/frontend/flora-occitania/src/app/services/authentication.service.ts new file mode 100644 index 0000000..e24121c --- /dev/null +++ b/frontend/flora-occitania/src/app/services/authentication.service.ts @@ -0,0 +1,123 @@ +import { Injectable } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { BehaviorSubject, Observable } from 'rxjs'; +import { map } from 'rxjs/operators'; +import { CookieService } from 'ng2-cookies'; + + +import {AppSettings} from '../appSettings'; + + +export interface User { + identifiant: string; + id_role: string; + id_organisme: number; + prenom_role?: string; + nom_role?: string; + nom_complet?: string; +} + + +@Injectable({ providedIn: 'root' }) +export class AuthenticationService { + private currentUserSubject: BehaviorSubject; + public currentUser: Observable; + token: string; + + constructor(private http: HttpClient, private _cookie: CookieService) { + this.currentUserSubject = new BehaviorSubject(this.getCurrentUser()); + this.currentUser = this.currentUserSubject.asObservable(); + } + + + public setCurrentUser(user) { + sessionStorage.setItem('currentUser', JSON.stringify(user)); + } + + getCurrentUser() { + let currentUser = sessionStorage.getItem('currentUser'); + + if (!currentUser) { + const userCookie = this._cookie.get('currentUser'); + if (userCookie !== '') { + let decodedCookie = this.decodeObjectCookies(userCookie); + decodedCookie = decodedCookie.split("'").join('"'); + this.setCurrentUser(decodedCookie); + currentUser = sessionStorage.getItem('currentUser'); + } + } + + return JSON.parse(currentUser); + } + + setToken(token, expireDate) { + this._cookie.set('token', token, expireDate); + } + + getToken() { + const token = this._cookie.get('token'); + const response = token.length === 0 ? null : token; + return response; + } + + + login(username: string, pwd: string) { + + const options = { + login: username, + password: pwd, + id_application: AppSettings.ID_APPLICATION + }; + return this.http.post(`${AppSettings.API_ENDPOINT}auth/login`, options) + .pipe(map(data => { + // login successful if there's a jwt token in the response + if (data) { + // store user details and jwt token in local storage to keep user logged in between page refreshes + const userForFront = { + identifiant: data.user.identifiant, + prenom_role: data.user.prenom_role, + id_role: data.user.id_role, + nom_role: data.user.nom_role, + nom_complet: data.user.nom_role + ' ' + data.user.prenom_role, + id_organisme: data.user.id_organisme + }; + sessionStorage.setItem('currentUser', JSON.stringify(data.user)); + this.currentUserSubject.next(userForFront); + this.currentUser = this.currentUserSubject.asObservable(); + } + + return data; + })); + } + + decodeObjectCookies(val) { + if (val.indexOf('\\') === -1) { + return val; // not encoded + } + val = val.slice(1, -1).replace(/\\"/g, '"'); + val = val.replace(/\\(\d{3})/g, function(match, octal) { + return String.fromCharCode(parseInt(octal, 8)); + }); + return val.replace(/\\\\/g, '\\'); + } + + deleteAllCookies() { + document.cookie.split(';').forEach(c => { + document.cookie = c + .replace(/^ +/, '') + .replace(/=.*/, '=;expires=' + new Date().toUTCString() + ';path=/'); + }); + } + + + isAuthenticated(): boolean { + return this._cookie.get('token') !== null; + } + + logout() { + this.deleteAllCookies(); + // remove user from local storage to log user out + sessionStorage.removeItem('currentUser'); + this.currentUserSubject.next(null); + } +} \ No newline at end of file diff --git a/frontend/flora-occitania/src/app/services/flora-occitania.service.ts b/frontend/flora-occitania/src/app/services/flora-occitania.service.ts index 52b75f4..349f16d 100644 --- a/frontend/flora-occitania/src/app/services/flora-occitania.service.ts +++ b/frontend/flora-occitania/src/app/services/flora-occitania.service.ts @@ -22,13 +22,13 @@ export class TaxonList { providedIn: 'root' }) export class FloraOccitaniaService { - sources:any; + sources: any; // Http Options httpOptions = { headers: new HttpHeaders({ 'Content-Type': 'application/json' - }) - } + }), withCredentials: true + }; constructor( private _http: HttpClient @@ -39,7 +39,7 @@ export class FloraOccitaniaService { } getSources(): Observable { - return this._http.get(AppSettings.API_ENDPOINT + "sources"); + return this._http.get(AppSettings.API_ENDPOINT + 'sources'); } getTaxonDetail(id): Observable { @@ -49,11 +49,10 @@ export class FloraOccitaniaService { postNomVern(cd_ref, data): Observable { const url = `${AppSettings.API_ENDPOINT}${cd_ref}`; - console.log(url); return this._http.post( url, - {params: data} - ) + {params: data, options: this.httpOptions} + ); } initSources(): Observable { diff --git a/frontend/flora-occitania/src/app/services/taxhub.service.ts b/frontend/flora-occitania/src/app/services/taxhub.service.ts index cccab20..c553d89 100644 --- a/frontend/flora-occitania/src/app/services/taxhub.service.ts +++ b/frontend/flora-occitania/src/app/services/taxhub.service.ts @@ -35,7 +35,7 @@ export class TaxhubService { } getTaxonDetail(id_nom: number): Observable { - const url = `${AppSettings.TAXHUB_ENDPOINT}bibnoms/${id_nom}`; + const url = `${AppSettings.TAXHUB_ENDPOINT}taxref/${id_nom}`; return this._http.get(url); } } diff --git a/frontend/flora-occitania/src/app/taxon-detail/display-foreignkey-array/display-foreignkey-array.component.html b/frontend/flora-occitania/src/app/taxon-detail/display-foreignkey-array/display-foreignkey-array.component.html index bf23fc6..9fae9ea 100644 --- a/frontend/flora-occitania/src/app/taxon-detail/display-foreignkey-array/display-foreignkey-array.component.html +++ b/frontend/flora-occitania/src/app/taxon-detail/display-foreignkey-array/display-foreignkey-array.component.html @@ -1,3 +1,4 @@
    -
  • {{val[keyLabel]}}
  • +
  • {{val['displayValue']}} +
diff --git a/frontend/flora-occitania/src/app/taxon-detail/display-foreignkey-array/display-foreignkey-array.component.ts b/frontend/flora-occitania/src/app/taxon-detail/display-foreignkey-array/display-foreignkey-array.component.ts index 28411c9..2422465 100644 --- a/frontend/flora-occitania/src/app/taxon-detail/display-foreignkey-array/display-foreignkey-array.component.ts +++ b/frontend/flora-occitania/src/app/taxon-detail/display-foreignkey-array/display-foreignkey-array.component.ts @@ -8,7 +8,7 @@ import { Component, OnInit, Input } from '@angular/core'; export class DisplayForeignkeyArrayComponent implements OnInit { @Input() valuesRef: Array; @Input() valuesIds: Array; - @Input() keyLabel: string; + @Input() keyLabel: Array; @Input() keyValue: string; toDisplay: Array = []; @@ -21,6 +21,22 @@ export class DisplayForeignkeyArrayComponent implements OnInit { return this.valuesIds.includes(val[this.keyValue]); } ); + + this.toDisplay.forEach((value, i, array) => { + const displayValue = []; + Object.keys(value).map( key => { + if (this.keyLabel.includes(key)) { + displayValue[this.keyLabel.indexOf(key)] = value[key]; + } + }); + displayValue.filter(v => {if (v) { return v; }}); + + this.toDisplay[i]['displayValue'] = displayValue.join(' - '); + }); + + this.toDisplay.sort((a, b) => { + return a.displayValue.localeCompare(b.displayValue); + }); } } diff --git a/frontend/flora-occitania/src/app/taxon-detail/taxon-detail.component.html b/frontend/flora-occitania/src/app/taxon-detail/taxon-detail.component.html index 3358264..a718dd0 100644 --- a/frontend/flora-occitania/src/app/taxon-detail/taxon-detail.component.html +++ b/frontend/flora-occitania/src/app/taxon-detail/taxon-detail.component.html @@ -1,9 +1,9 @@
-

+

-
- Editer nom vern + @@ -11,7 +11,7 @@

Info taxon

cd_nom : {{taxon.cd_nom}}
cd_ref : {{taxon.cd_ref}}
-
nom_vern : {{taxon.taxref.nom_vern}}
+
nom_vern : {{taxon.nom_vern}}
@@ -41,29 +41,29 @@

Noms vernaculaires

Localisations
Parties utilisees
Usages
-
Commentaire usages{{nom.commentaire_usage}}
+
Commentaire usages{{nom.commentaire_usage}}
Sources
diff --git a/frontend/flora-occitania/src/app/taxon-detail/taxon-detail.component.ts b/frontend/flora-occitania/src/app/taxon-detail/taxon-detail.component.ts index dd2929a..bf072bc 100644 --- a/frontend/flora-occitania/src/app/taxon-detail/taxon-detail.component.ts +++ b/frontend/flora-occitania/src/app/taxon-detail/taxon-detail.component.ts @@ -6,7 +6,7 @@ import { Location } from '@angular/common'; import {FloraOccitaniaService, TaxonList} from "../services/flora-occitania.service" import {TaxhubService} from "../services/taxhub.service" import { NomenclatureService } from '../services/nomenclature.service'; - +import { AuthenticationService, User } from '../services/authentication.service'; @Component({ selector: 'app-taxon-detail', @@ -18,14 +18,18 @@ export class TaxonDetailComponent implements OnInit { taxon: any; nomVerns: any; nomenclatureValue: any; + currentUser: User; constructor( public floraOccitaniaService: FloraOccitaniaService, public taxhubService: TaxhubService, private route: ActivatedRoute, public nomenclatureService: NomenclatureService, - private location: Location - ) { } + private location: Location, + private authenticationService: AuthenticationService + ) { + this.authenticationService.currentUser.subscribe(x => this.currentUser = x); + } ngOnInit() { this.getTaxon(); diff --git a/frontend/package-lock.json b/frontend/package-lock.json new file mode 100644 index 0000000..48e341a --- /dev/null +++ b/frontend/package-lock.json @@ -0,0 +1,3 @@ +{ + "lockfileVersion": 1 +} diff --git a/install.sh b/install.sh index 7852aa5..1ec4544 100755 --- a/install.sh +++ b/install.sh @@ -6,6 +6,13 @@ echo "Arret de l'application..." sudo -s supervisorctl stop ${app_name} + +# make nvm available +export NVM_DIR="$HOME/.nvm" +[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" # This loads nvm +[ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion" # This loads nvm bash_completion + + #Création des répertoires systèmes mkdir -p var/log @@ -23,7 +30,7 @@ sed -i "s/SQLALCHEMY_DATABASE_URI = .*$/SQLALCHEMY_DATABASE_URI = \"postgresql:\ rm -r venv -virtualenv -p /usr/bin/python3 venv +python3 -m venv venv source venv/bin/activate pip install --upgrade pip @@ -35,7 +42,14 @@ cd .. # Front end -cd frontend/flora-occitania/ +cd frontend + +nvm install +nvm use + + +cd flora-occitania/ + #création d'un fichier de configuration if [ ! -f src/app/appSettings.ts ]; then echo 'Fichier de configuration non existant' @@ -53,4 +67,4 @@ sudo -s cp flora_occitania-service.conf /etc/supervisor/conf.d/ sudo -s sed -i "s%APP_PATH%${DIR}%" /etc/supervisor/conf.d/flora_occitania-service.conf sudo -s supervisorctl reread -sudo -s supervisorctl reload \ No newline at end of file +sudo -s supervisorctl reload diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..276ffce --- /dev/null +++ b/setup.py @@ -0,0 +1,40 @@ +import setuptools +from pathlib import Path + + +root_dir = Path(__file__).absolute().parent +with (root_dir / "VERSION").open() as f: + version = f.read().strip() + +setuptools.setup( + name="flora_occitania", + description="", + long_description="Affichage et saisie de données éthonobotaniques en Occitan", + long_description_content_type="text/markdown", + maintainer="Parc national des Cévennes", + maintainer_email="admin_si@cevennes-parcnational.fr", + url="https://github.com/PnX-SI/Flora_occitania/", + python_requires=">=3.8", + version=version, + packages=setuptools.find_packages(where="backend", include=["flora_occitania"]), + package_dir={ + "": "backend", + }, + install_requires=list( + open("backend/requirements.txt", "r"), + ), + extras_require={ + "tests": [ + "pytest", + "pytest-flask", + "pytest-benchmark", + "pytest-cov", + ], + "doc": [], + }, + classifiers=[ + "Framework :: Flask", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.11", + ], +)