From 5443c3ed0e48c8fa31edef523da80817042463e2 Mon Sep 17 00:00:00 2001 From: amandine-sahl Date: Tue, 22 Nov 2022 18:44:13 +0100 Subject: [PATCH 01/25] =?UTF-8?q?Feat=20taxhub=20v2=20:=20mod=C3=A9lisatio?= =?UTF-8?q?n=20suppression=20de=20la=20table=20bib=5Fnoms?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...d64c7_delete_bib_noms_id_nom_dependancy.py | 68 +++ ...734f490ff_delete_bib_noms_cor_nom_liste.py | 64 +++ ...8be_change_tmedia_before_insert_trigger.py | 53 +++ apptax/taxonomie/models.py | 110 ++--- apptax/taxonomie/routesbibattributs.py | 2 +- apptax/taxonomie/routesbiblistes.py | 49 +- apptax/taxonomie/routesbibnoms.py | 432 +++++++++--------- apptax/taxonomie/routestaxref.py | 84 ++-- apptax/tests/fixtures.py | 26 +- apptax/tests/test_taxref.py | 10 +- 10 files changed, 575 insertions(+), 323 deletions(-) create mode 100644 apptax/migrations/versions/73306d6d64c7_delete_bib_noms_id_nom_dependancy.py create mode 100644 apptax/migrations/versions/b7d734f490ff_delete_bib_noms_cor_nom_liste.py create mode 100644 apptax/migrations/versions/b9e157ffd8be_change_tmedia_before_insert_trigger.py diff --git a/apptax/migrations/versions/73306d6d64c7_delete_bib_noms_id_nom_dependancy.py b/apptax/migrations/versions/73306d6d64c7_delete_bib_noms_id_nom_dependancy.py new file mode 100644 index 000000000..a526b8520 --- /dev/null +++ b/apptax/migrations/versions/73306d6d64c7_delete_bib_noms_id_nom_dependancy.py @@ -0,0 +1,68 @@ +"""delete bib_noms id_nom dependancy + +Revision ID: 73306d6d64c7 +Revises: b7d734f490ff +Create Date: 2022-11-22 17:13:15.380256 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '73306d6d64c7' +down_revision = 'b7d734f490ff' +branch_labels = None +depends_on = None + + +def upgrade(): + op.execute(""" + CREATE OR REPLACE VIEW taxonomie.v_taxref_all_listes AS + SELECT t.regne, + t.phylum, + t.classe, + t.ordre, + t.famille, + t.group1_inpn, + t.group2_inpn, + t.cd_nom, + t.cd_ref, + t.nom_complet, + t.nom_valide, + t.nom_vern AS nom_vern, + t.lb_nom, + d.id_liste + FROM taxonomie.taxref t + JOIN taxonomie.cor_nom_liste d ON t.cd_nom = d.cd_nom; + """) + + +def downgrade(): + op.execute(""" + CREATE OR REPLACE VIEW taxonomie.v_taxref_all_listes + AS WITH bib_nom_lst AS ( + SELECT cor_nom_liste.id_nom, + bib_noms.cd_nom, + bib_noms.nom_francais, + cor_nom_liste.id_liste + FROM taxonomie.cor_nom_liste + JOIN taxonomie.bib_noms USING (id_nom) + ) + SELECT t.regne, + t.phylum, + t.classe, + t.ordre, + t.famille, + t.group1_inpn, + t.group2_inpn, + t.cd_nom, + t.cd_ref, + t.nom_complet, + t.nom_valide, + d.nom_francais AS nom_vern, + t.lb_nom, + d.id_liste + FROM taxonomie.taxref t + JOIN bib_nom_lst d ON t.cd_nom = d.cd_nom; + """) diff --git a/apptax/migrations/versions/b7d734f490ff_delete_bib_noms_cor_nom_liste.py b/apptax/migrations/versions/b7d734f490ff_delete_bib_noms_cor_nom_liste.py new file mode 100644 index 000000000..111a6c3d8 --- /dev/null +++ b/apptax/migrations/versions/b7d734f490ff_delete_bib_noms_cor_nom_liste.py @@ -0,0 +1,64 @@ +"""delete bib_noms cor_nom_liste + +Revision ID: b7d734f490ff +Revises: 64d38dbe7739 +Create Date: 2022-11-22 16:50:15.520049 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = 'b7d734f490ff' +down_revision = '64d38dbe7739' +branch_labels = None +depends_on = None + + +def upgrade(): + op.execute( + """ + ALTER TABLE taxonomie.cor_nom_liste ADD cd_nom int; + + UPDATE taxonomie.cor_nom_liste AS cnl SET cd_nom = bn.cd_nom + FROM taxonomie.bib_noms AS bn + WHERE bn.id_nom = cnl.id_nom; + + ALTER TABLE taxonomie.cor_nom_liste DROP CONSTRAINT cor_nom_liste_pkey; + ALTER TABLE taxonomie.cor_nom_liste DROP CONSTRAINT unique_cor_nom_liste_id_liste_id_nom; + ALTER TABLE taxonomie.cor_nom_liste DROP CONSTRAINT cor_nom_listes_bib_listes_fkey; + ALTER TABLE taxonomie.cor_nom_liste DROP CONSTRAINT cor_nom_listes_bib_noms_fkey; + + ALTER TABLE taxonomie.cor_nom_liste ADD CONSTRAINT cor_nom_liste_pkey PRIMARY KEY (cd_nom, id_liste); + ALTER TABLE taxonomie.cor_nom_liste ADD CONSTRAINT unique_cor_nom_liste_id_liste_cd_nom UNIQUE (id_liste, cd_nom); + ALTER TABLE taxonomie.cor_nom_liste ADD CONSTRAINT cor_nom_listes_bib_listes_fkey FOREIGN KEY (id_liste) REFERENCES taxonomie.bib_listes(id_liste) ON UPDATE CASCADE; + ALTER TABLE taxonomie.cor_nom_liste ADD CONSTRAINT cor_nom_listes_taxref_fkey FOREIGN KEY (cd_nom) REFERENCES taxonomie.taxref(cd_nom) ON DELETE CASCADE ON UPDATE CASCADE; + + + ALTER TABLE taxonomie.cor_nom_liste ALTER COLUMN id_nom DROP NOT NULL; + + """ + ) + + +def downgrade(): + op.execute( + """ + ALTER TABLE taxonomie.cor_nom_liste ADD CONSTRAINT cor_nom_liste_pkey; + ALTER TABLE taxonomie.cor_nom_liste ADD CONSTRAINT unique_cor_nom_liste_id_liste_id_nom; + ALTER TABLE taxonomie.cor_nom_liste ADD CONSTRAINT cor_nom_listes_bib_listes_fkey; + ALTER TABLE taxonomie.cor_nom_liste ADD CONSTRAINT cor_nom_listes_bib_noms_fkey; + + ALTER TABLE taxonomie.cor_nom_liste DROP CONSTRAINT cor_nom_liste_pkey PRIMARY KEY (cd_nom, id_liste); + ALTER TABLE taxonomie.cor_nom_liste DROP CONSTRAINT unique_cor_nom_liste_id_liste_cd_nom UNIQUE (id_liste, cd_nom); + ALTER TABLE taxonomie.cor_nom_liste DROP CONSTRAINT cor_nom_listes_bib_listes_fkey FOREIGN KEY (id_liste) REFERENCES taxonomie.bib_listes(id_liste) ON UPDATE CASCADE; + ALTER TABLE taxonomie.cor_nom_liste DROP CONSTRAINT cor_nom_listes_taxref_fkey FOREIGN KEY (cd_nom) REFERENCES taxonomie.taxref(cd_nom) ON DELETE CASCADE ON UPDATE CASCADE; + + + ALTER TABLE taxonomie.cor_nom_liste ALTER COLUMN id_nom ADD NOT NULL; + + + ALTER TABLE taxonomie.cor_nom_liste DROP cd_nom; + """ + ) diff --git a/apptax/migrations/versions/b9e157ffd8be_change_tmedia_before_insert_trigger.py b/apptax/migrations/versions/b9e157ffd8be_change_tmedia_before_insert_trigger.py new file mode 100644 index 000000000..85e642a76 --- /dev/null +++ b/apptax/migrations/versions/b9e157ffd8be_change_tmedia_before_insert_trigger.py @@ -0,0 +1,53 @@ +"""Change tmedia before insert trigger + +Revision ID: b9e157ffd8be +Revises: 73306d6d64c7 +Create Date: 2022-11-22 17:41:07.543733 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = 'b9e157ffd8be' +down_revision = '73306d6d64c7' +branch_labels = None +depends_on = None + + +def upgrade(): + op.execute(""" + CREATE OR REPLACE FUNCTION taxonomie.insert_t_medias() + RETURNS trigger + LANGUAGE plpgsql + AS $function$ + DECLARE + trimtitre text; + BEGIN + new.date_media = now(); + new.cd_ref = taxonomie.find_cdref(new.cd_ref); + trimtitre = replace(new.titre, ' ', ''); + RETURN NEW; + END; + $function$ + ; + """) + + +def downgrade(): + op.execute(""" + CREATE OR REPLACE FUNCTION taxonomie.insert_t_medias() + RETURNS trigger + LANGUAGE plpgsql + AS $function$ + DECLARE + trimtitre text; + BEGIN + new.date_media = now(); + trimtitre = replace(new.titre, ' ', ''); + RETURN NEW; + END; + $function$ + ; + """) diff --git a/apptax/taxonomie/models.py b/apptax/taxonomie/models.py index 35e261dfb..0adb2e173 100644 --- a/apptax/taxonomie/models.py +++ b/apptax/taxonomie/models.py @@ -11,26 +11,6 @@ from . import db - -@serializable -class BibNoms(db.Model): - __tablename__ = "bib_noms" - __table_args__ = {"schema": "taxonomie"} - id_nom = db.Column(db.Integer, primary_key=True) - cd_nom = db.Column(db.Integer, ForeignKey("taxonomie.taxref.cd_nom"), nullable=True) - cd_ref = db.Column(db.Integer) - nom_francais = db.Column(db.Unicode) - comments = db.Column(db.Unicode) - - taxref = db.relationship("Taxref", back_populates="bib_nom") - attributs = db.relationship("CorTaxonAttribut", back_populates="bib_nom") - listes = db.relationship("CorNomListe", back_populates="bib_nom") - # medias relationship defined through backref - - def __repr__(self): - return f"" - - @serializable class CorTaxonAttribut(db.Model): __tablename__ = "cor_taxon_attribut" @@ -43,12 +23,11 @@ class CorTaxonAttribut(db.Model): ) cd_ref = db.Column( db.Integer, - ForeignKey("taxonomie.bib_noms.cd_ref"), + ForeignKey("taxonomie.taxref.cd_ref"), nullable=False, primary_key=True, ) valeur_attribut = db.Column(db.Text, nullable=False) - bib_nom = db.relationship("BibNoms", back_populates="attributs") bib_attribut = db.relationship("BibAttributs") def __repr__(self): @@ -96,6 +75,54 @@ class BibAttributs(db.Model): def __repr__(self): return f"" +@serializable +class CorNomListe(db.Model): + __tablename__ = "cor_nom_liste" + __table_args__ = {"schema": "taxonomie"} + id_liste = db.Column( + db.Integer, + ForeignKey("taxonomie.bib_listes.id_liste"), + nullable=False, + primary_key=True, + ) + cd_nom = db.Column( + db.Integer, + ForeignKey("taxonomie.taxref.cd_nom"), + nullable=False, + primary_key=True, + ) + + taxref = db.relationship("Taxref") + + bib_liste = db.relationship("BibListes") + + def __repr__(self): + return "" % self.id_liste + +@serializable +class CorNomListe(db.Model): + __tablename__ = "cor_nom_liste" + __table_args__ = {"schema": "taxonomie"} + id_liste = db.Column( + db.Integer, + ForeignKey("taxonomie.bib_listes.id_liste"), + nullable=False, + primary_key=True, + ) + cd_nom = db.Column( + db.Integer, + ForeignKey("taxonomie.taxref.cd_nom"), + nullable=False, + primary_key=True, + ) + + taxref = db.relationship("Taxref") + + bib_liste = db.relationship("BibListes") + + def __repr__(self): + return "" % self.id_liste + @serializable(exclude=["nom_vern_or_lb_nom"]) class Taxref(db.Model): @@ -128,7 +155,7 @@ class Taxref(db.Model): group3_inpn = db.Column(db.Unicode) url = db.Column(db.Unicode) - bib_nom = db.relationship(BibNoms, back_populates="taxref") + liste = db.relationship("BibListes", secondary=CorNomListe.__table__) @hybrid_property def nom_vern_or_lb_nom(self): @@ -142,28 +169,6 @@ def __repr__(self): return f"" -@serializable -class CorNomListe(db.Model): - __tablename__ = "cor_nom_liste" - __table_args__ = {"schema": "taxonomie"} - id_liste = db.Column( - db.Integer, - ForeignKey("taxonomie.bib_listes.id_liste"), - nullable=False, - primary_key=True, - ) - id_nom = db.Column( - db.Integer, - ForeignKey("taxonomie.bib_noms.id_nom"), - nullable=False, - primary_key=True, - ) - bib_nom = db.relationship(BibNoms, back_populates="listes") - bib_liste = db.relationship("BibListes", back_populates="cnl") - - def __repr__(self): - return f"" - @serializable class BibListes(db.Model): @@ -177,10 +182,8 @@ class BibListes(db.Model): regne = db.Column(db.Unicode) group2_inpn = db.Column(db.Unicode) - cnl = db.relationship(CorNomListe, lazy="select", back_populates="bib_liste") - noms = db.relationship( - "BibNoms", secondary=CorNomListe.__table__, overlaps="bib_nom,listes,bib_liste,cnl" - ) + cnl = db.relationship("CorNomListe", lazy="select") + noms = db.relationship("Taxref", secondary=CorNomListe.__table__) def __repr__(self): return f"" @@ -205,7 +208,7 @@ class TMedias(db.Model): id_media = db.Column(db.Integer, primary_key=True) cd_ref = db.Column( db.Integer, - ForeignKey(BibNoms.cd_nom), + ForeignKey(Taxref.cd_ref), nullable=False, primary_key=False, ) @@ -225,8 +228,11 @@ class TMedias(db.Model): ) types = db.relationship(BibTypesMedia) - bib_nom = db.relationship(BibNoms, backref="medias") - + # bib_nom = db.relationship(BibNoms, backref="medias") + taxon = db.relationship( + Taxref, + backref="medias" + ) def __repr__(self): return f"" diff --git a/apptax/taxonomie/routesbibattributs.py b/apptax/taxonomie/routesbibattributs.py index ec9e81c4b..cd808f9e6 100644 --- a/apptax/taxonomie/routesbibattributs.py +++ b/apptax/taxonomie/routesbibattributs.py @@ -4,7 +4,7 @@ from sqlalchemy import select, or_ from ..utils.utilssqlalchemy import json_resp -from .models import BibNoms, Taxref, CorTaxonAttribut, BibAttributs +from .models import Taxref, CorTaxonAttribut, BibAttributs from . import db diff --git a/apptax/taxonomie/routesbiblistes.py b/apptax/taxonomie/routesbiblistes.py index 922230696..74a9c66ee 100644 --- a/apptax/taxonomie/routesbiblistes.py +++ b/apptax/taxonomie/routesbiblistes.py @@ -14,7 +14,7 @@ from ..log import logmanager from ..utils.utilssqlalchemy import json_resp, csv_resp from ..utils.genericfunctions import calculate_offset_page -from .models import BibListes, CorNomListe, Taxref, BibNoms +from .models import BibListes, CorNomListe, Taxref adresses = Blueprint("bib_listes", __name__) @@ -66,10 +66,10 @@ def getExporter_biblistesCSV(idliste=None): liste = db.session.query(BibListes).get(idliste) cleanNomliste = filemanager.removeDisallowedFilenameChars(liste.nom_liste) + # @DEL_BIB_NOM data = ( db.session.query(Taxref) - .filter(BibNoms.cd_nom == Taxref.cd_nom) - .filter(BibNoms.id_nom == CorNomListe.id_nom) + .filter(Taxref.cd_nom == CorNomListe.id_nom) .filter(CorNomListe.id_liste == idliste) .all() ) @@ -143,16 +143,13 @@ def getNoms_bibtaxons(idliste): .one() ) + # @DEL_BIB_NOM q = db.session.query( - BibNoms.id_nom, - BibNoms.cd_nom, - BibNoms.cd_ref, - BibNoms.nom_francais, Taxref.nom_complet, Taxref.regne, Taxref.group2_inpn, Taxref.id_rang, - ).filter(BibNoms.cd_nom == Taxref.cd_nom) + ) if regne: q = q.filter(or_(Taxref.regne == regne)) @@ -160,33 +157,39 @@ def getNoms_bibtaxons(idliste): q = q.filter(or_(Taxref.group2_inpn == group2_inpn)) subq = db.session.query(CorNomListe.id_nom).filter(CorNomListe.id_liste == idliste).subquery() - if parameters.get("existing"): - q = q.join(subq, subq.c.id_nom == BibNoms.id_nom) - else: - q = q.outerjoin(subq, subq.c.id_nom == BibNoms.id_nom).filter(subq.c.id_nom == None) + + + # @DEL_BIB_NOM ??? + # if parameters.get("existing"): + # q = q.join(subq, subq.c.id_nom == BibNoms.id_nom) + # else: + # q = q.outerjoin(subq, subq.c.id_nom == BibNoms.id_nom).filter(subq.c.id_nom == None) nbResultsWithoutFilter = q.count() if parameters.get("cd_nom"): try: - q = q.filter(BibNoms.cd_nom == int(parameters.get("cd_nom"))) + q = q.filter(Taxref.cd_nom == int(parameters.get("cd_nom"))) except Exception: pass + # @DEL_BIB_NOM if parameters.get("nom_francais"): - q = q.filter(BibNoms.nom_francais.ilike(parameters.get("nom_francais") + "%")) + q = q.filter(Taxref.nom_vern.ilike(parameters.get("nom_francais") + "%")) if parameters.get("nom_complet"): q = q.filter(Taxref.nom_complet.ilike(parameters.get("nom_complet") + "%")) if parameters.get("id_rang"): q = q.filter(Taxref.id_rang.ilike(parameters.get("id_rang") + "%")) # Order by - bibTaxonColumns = BibNoms.__table__.columns + # @DEL_BIB_NOM + # bibTaxonColumns = BibNoms.__table__.columns taxrefColumns = Taxref.__table__.columns if "orderby" in parameters: if parameters["orderby"] in taxrefColumns: orderCol = getattr(taxrefColumns, parameters["orderby"]) - elif parameters["orderby"] in bibTaxonColumns: - orderCol = getattr(bibTaxonColumns, parameters["orderby"]) + # @DEL_BIB_NOM + # elif parameters["orderby"] in bibTaxonColumns: + # orderCol = getattr(bibTaxonColumns, parameters["orderby"]) else: orderCol = None @@ -202,10 +205,11 @@ def getNoms_bibtaxons(idliste): results = [] for row in data: data_as_dict = {} - data_as_dict["id_nom"] = row.id_nom + # @DEL_BIB_NOM + # data_as_dict["id_nom"] = row.id_nom data_as_dict["cd_nom"] = row.cd_nom data_as_dict["cd_ref"] = row.cd_ref - data_as_dict["nom_francais"] = row.nom_francais + data_as_dict["nom_francais"] = row.nom_vern data_as_dict["nom_complet"] = row.nom_complet data_as_dict["regne"] = row.regne data_as_dict["group2_inpn"] = row.group2_inpn @@ -229,9 +233,9 @@ def add_cornomliste(idliste=None): ids_nom = request.get_json(silent=True) # TODO add test if idlist exists - + # @DEL_BIB_NOM for id in ids_nom: - cornom = {"id_nom": id, "id_liste": idliste} + cornom = {"cd_nom": id, "id_liste": idliste} add_nom = CorNomListe(**cornom) db.session.add(add_nom) db.session.commit() @@ -246,11 +250,12 @@ def add_cornomliste(idliste=None): @fnauth.check_auth(4) def delete_cornomliste(idliste=None): ids_nom = request.get_json(silent=True) + # @DEL_BIB_NOM for id in ids_nom: del_nom = ( db.session.query(CorNomListe) .filter(CorNomListe.id_liste == idliste) - .filter(CorNomListe.id_nom == id) + .filter(CorNomListe.cd_nom == id) .first() ) db.session.delete(del_nom) diff --git a/apptax/taxonomie/routesbibnoms.py b/apptax/taxonomie/routesbibnoms.py index 5cc150aed..13828853a 100644 --- a/apptax/taxonomie/routesbibnoms.py +++ b/apptax/taxonomie/routesbibnoms.py @@ -9,7 +9,7 @@ from ..utils.genericfunctions import calculate_offset_page from ..log import logmanager from .models import ( - BibNoms, + Taxref, CorTaxonAttribut, BibThemes, @@ -27,69 +27,69 @@ media_repo = MediaRepository(db.session, current_app.config.get("S3_BUCKET_NAME")) -@adresses.route("/", methods=["GET"]) -@json_resp -def get_bibtaxons(): - bibTaxonColumns = BibNoms.__table__.columns - taxrefColumns = Taxref.__table__.columns - parameters = request.args - - q = db.session.query(BibNoms, Taxref).filter(BibNoms.cd_nom == Taxref.cd_nom) - - nbResultsWithoutFilter = q.count() - - # Traitement des parametres - limit = parameters.get("limit", 20, int) - page = parameters.get("page", 1, int) - offset = parameters.get("offset", 0, int) - (limit, offset, page) = calculate_offset_page(limit, offset, page) - - # Order by - if "orderby" in parameters: - if parameters["orderby"] in taxrefColumns: - orderCol = getattr(taxrefColumns, parameters["orderby"]) - elif parameters["orderby"] in bibTaxonColumns: - orderCol = getattr(bibTaxonColumns, parameters["orderby"]) - else: - orderCol = None - - if "order" in parameters: - if parameters["order"] == "desc": - orderCol = orderCol.desc() - - q = q.order_by(orderCol) - - for param in parameters: - if param in taxrefColumns: - col = getattr(taxrefColumns, param) - q = q.filter(col == parameters[param]) - elif param in bibTaxonColumns: - col = getattr(bibTaxonColumns, param) - q = q.filter(col == parameters[param]) - elif param == "ilikelatin": - q = q.filter(taxrefColumns.nom_complet.ilike(parameters[param] + "%")) - elif param == "ilikelfr": - q = q.filter(bibTaxonColumns.nom_francais.ilike(parameters[param] + "%")) - elif param == "ilikeauteur": - q = q.filter(taxrefColumns.lb_auteur.ilike(parameters[param] + "%")) - elif (param == "is_ref") and (parameters[param] == "true"): - q = q.filter(taxrefColumns.cd_nom == taxrefColumns.cd_ref) - - nbResults = q.count() - data = q.limit(limit).offset(offset).all() - results = [] - for row in data: - data_as_dict = row.BibNoms.as_dict() - data_as_dict["taxref"] = row.Taxref.as_dict() - results.append(data_as_dict) - # {"data":results,"count":0} - return { - "items": results, - "total": nbResultsWithoutFilter, - "total_filtered": nbResults, - "limit": limit, - "page": page, - } +# @adresses.route("/", methods=["GET"]) +# @json_resp +# def get_bibtaxons(): +# bibTaxonColumns = BibNoms.__table__.columns +# taxrefColumns = Taxref.__table__.columns +# parameters = request.args + +# q = db.session.query(BibNoms, Taxref).filter(BibNoms.cd_nom == Taxref.cd_nom) + +# nbResultsWithoutFilter = q.count() + +# # Traitement des parametres +# limit = parameters.get("limit", 20, int) +# page = parameters.get("page", 1, int) +# offset = parameters.get("offset", 0, int) +# (limit, offset, page) = calculate_offset_page(limit, offset, page) + +# # Order by +# if "orderby" in parameters: +# if parameters["orderby"] in taxrefColumns: +# orderCol = getattr(taxrefColumns, parameters["orderby"]) +# elif parameters["orderby"] in bibTaxonColumns: +# orderCol = getattr(bibTaxonColumns, parameters["orderby"]) +# else: +# orderCol = None + +# if "order" in parameters: +# if parameters["order"] == "desc": +# orderCol = orderCol.desc() + +# q = q.order_by(orderCol) + +# for param in parameters: +# if param in taxrefColumns: +# col = getattr(taxrefColumns, param) +# q = q.filter(col == parameters[param]) +# elif param in bibTaxonColumns: +# col = getattr(bibTaxonColumns, param) +# q = q.filter(col == parameters[param]) +# elif param == "ilikelatin": +# q = q.filter(taxrefColumns.nom_complet.ilike(parameters[param] + "%")) +# elif param == "ilikelfr": +# q = q.filter(bibTaxonColumns.nom_francais.ilike(parameters[param] + "%")) +# elif param == "ilikeauteur": +# q = q.filter(taxrefColumns.lb_auteur.ilike(parameters[param] + "%")) +# elif (param == "is_ref") and (parameters[param] == "true"): +# q = q.filter(taxrefColumns.cd_nom == taxrefColumns.cd_ref) + +# nbResults = q.count() +# data = q.limit(limit).offset(offset).all() +# results = [] +# for row in data: +# data_as_dict = row.BibNoms.as_dict() +# data_as_dict["taxref"] = row.Taxref.as_dict() +# results.append(data_as_dict) +# # {"data":results,"count":0} +# return { +# "items": results, +# "total": nbResultsWithoutFilter, +# "total_filtered": nbResults, +# "limit": limit, +# "page": page, +# } @adresses.route("/taxoninfo/", methods=["GET"]) @@ -142,155 +142,155 @@ def getOne_bibtaxonsInfo(cd_nom): return obj -@adresses.route("/simple/", methods=["GET"]) -@json_resp -def getOneSimple_bibtaxons(id_nom): - bibTaxon = db.session.query(BibNoms).filter_by(id_nom=id_nom).first() - obj = bibTaxon.as_dict() - - # Ajout des listes - obj["listes"] = [] - for liste in bibTaxon.listes: - o = dict(liste.as_dict().items()) - o.update(dict(liste.bib_liste.as_dict().items())) - obj["listes"].append(o) - - return obj - - -@adresses.route("/", methods=["GET"]) -@json_resp -def getOneFull_bibtaxons(id_nom): - bibTaxon = db.session.query(BibNoms).filter_by(id_nom=id_nom).first() - - obj = bibTaxon.as_dict() - - # Ajout des synonymes - obj["is_doublon"] = False - (nbsyn, results) = getBibTaxonSynonymes(id_nom, bibTaxon.cd_nom) - if nbsyn > 0: - obj["is_doublon"] = True - obj["synonymes"] = [i.id_nom for i in results] - - # Ajout des attributs - obj["attributs"] = [] - for attr in bibTaxon.attributs: - o = dict(attr.as_dict().items()) - o.update(dict(attr.bib_attribut.as_dict().items())) - id = o["id_theme"] - theme = db.session.query(BibThemes).filter_by(id_theme=id).first() - o["nom_theme"] = theme.as_dict()["nom_theme"] - obj["attributs"].append(o) - - # Ajout des donnees taxref - obj["taxref"] = bibTaxon.taxref.as_dict() - - # Ajout des listes - obj["listes"] = [] - for liste in bibTaxon.listes: - o = dict(liste.as_dict().items()) - o.update(dict(liste.bib_liste.as_dict().items())) - obj["listes"].append(o) - - # Ajout des medias - obj["medias"] = [] - for medium in bibTaxon.medias: - o = dict(medium.as_dict().items()) - o.update(dict(medium.types.as_dict().items())) - obj["medias"].append(o) - return obj - - -@adresses.route("/", methods=["POST", "PUT"]) -@adresses.route("/", methods=["POST", "PUT"]) -@fnauth.check_auth(3) -def insertUpdate_bibtaxons(id_nom=None): - data = request.get_json(silent=True) - - if id_nom: - bibTaxon = db.session.query(BibNoms).filter_by(id_nom=id_nom).first() - - bibTaxon.nom_francais = data["nom_francais"] if "nom_francais" in data else None - bibTaxon.comments = data["comments"] if "comments" in data else None - action = "UPDATE" - message = "Taxon mis à jour" - else: - bibTaxon = BibNoms( - cd_nom=data["cd_nom"], - cd_ref=data["cd_ref"], - nom_francais=data["nom_francais"] if "nom_francais" in data else None, - comments=data["comments"] if "comments" in data else None, - ) - action = "INSERT" - message = "Taxon ajouté" - - db.session.add(bibTaxon) - db.session.commit() - - id_nom = bibTaxon.id_nom - - # ###--------------Traitement des attibuts----------------- - # Suppression des attributs existants - for bibTaxonAtt in bibTaxon.attributs: - db.session.delete(bibTaxonAtt) - - db.session.flush() - - if "attributs_values" in data: - for att in data["attributs_values"]: - if data["attributs_values"][att] != "" and data["attributs_values"][att] is not None: - attVal = CorTaxonAttribut( - id_attribut=att, - cd_ref=bibTaxon.cd_ref, - valeur_attribut=data["attributs_values"][att], - ) - db.session.add(attVal) - - db.session.commit() - - # ###--------------Traitement des listes----------------- - # Suppression des listes existantes - for bibTaxonLst in bibTaxon.listes: - db.session.delete(bibTaxonLst) - - db.session.flush() - - if "listes" in data: - for lst in data["listes"]: - listTax = CorNomListe(id_liste=lst["id_liste"], id_nom=id_nom) - db.session.add(listTax) - - # Log - logmanager.log_action("bib_nom", id_nom, repr(bibTaxon), action, message) - db.session.commit() - return ( - json.dumps({"success": True, "id_nom": id_nom}), - 200, - {"ContentType": "application/json"}, - ) - - -@adresses.route("/", methods=["DELETE"]) -@fnauth.check_auth(6) -@json_resp -def delete_bibtaxons(id_nom): - bibTaxon = db.session.query(BibNoms).filter_by(id_nom=id_nom).first() - db.session.delete(bibTaxon) - db.session.commit() - - # #Log - logmanager.log_action("bib_nom", id_nom, repr(bibTaxon), "DELETE", "nom supprimé") - - return bibTaxon.as_dict() - - -# Private functions -def getBibTaxonSynonymes(id_nom, cd_nom): - q = ( - db.session.query(BibNoms.id_nom) - .join(BibNoms.taxref) - .filter(Taxref.cd_ref == func.taxonomie.find_cdref(cd_nom)) - .filter(BibNoms.id_nom != id_nom) - ) - results = q.all() - return (q.count(), results) +# @adresses.route("/simple/", methods=["GET"]) +# @json_resp +# def getOneSimple_bibtaxons(id_nom): +# bibTaxon = db.session.query(BibNoms).filter_by(id_nom=id_nom).first() +# obj = bibTaxon.as_dict() + +# # Ajout des listes +# obj["listes"] = [] +# for liste in bibTaxon.listes: +# o = dict(liste.as_dict().items()) +# o.update(dict(liste.bib_liste.as_dict().items())) +# obj["listes"].append(o) + +# return obj + + +# @adresses.route("/", methods=["GET"]) +# @json_resp +# def getOneFull_bibtaxons(id_nom): +# bibTaxon = db.session.query(BibNoms).filter_by(id_nom=id_nom).first() + +# obj = bibTaxon.as_dict() + +# # Ajout des synonymes +# obj["is_doublon"] = False +# (nbsyn, results) = getBibTaxonSynonymes(id_nom, bibTaxon.cd_nom) +# if nbsyn > 0: +# obj["is_doublon"] = True +# obj["synonymes"] = [i.id_nom for i in results] + +# # Ajout des attributs +# obj["attributs"] = [] +# for attr in bibTaxon.attributs: +# o = dict(attr.as_dict().items()) +# o.update(dict(attr.bib_attribut.as_dict().items())) +# id = o["id_theme"] +# theme = db.session.query(BibThemes).filter_by(id_theme=id).first() +# o["nom_theme"] = theme.as_dict()["nom_theme"] +# obj["attributs"].append(o) + +# # Ajout des donnees taxref +# obj["taxref"] = bibTaxon.taxref.as_dict() + +# # Ajout des listes +# obj["listes"] = [] +# for liste in bibTaxon.listes: +# o = dict(liste.as_dict().items()) +# o.update(dict(liste.bib_liste.as_dict().items())) +# obj["listes"].append(o) + +# # Ajout des medias +# obj["medias"] = [] +# for medium in bibTaxon.medias: +# o = dict(medium.as_dict().items()) +# o.update(dict(medium.types.as_dict().items())) +# obj["medias"].append(o) +# return obj + + +# @adresses.route("/", methods=["POST", "PUT"]) +# @adresses.route("/", methods=["POST", "PUT"]) +# @fnauth.check_auth(3, True) +# def insertUpdate_bibtaxons(id_nom=None, id_role=None): +# data = request.get_json(silent=True) + +# if id_nom: +# bibTaxon = db.session.query(BibNoms).filter_by(id_nom=id_nom).first() + +# bibTaxon.nom_francais = data["nom_francais"] if "nom_francais" in data else None +# bibTaxon.comments = data["comments"] if "comments" in data else None +# action = "UPDATE" +# message = "Taxon mis à jour" +# else: +# bibTaxon = BibNoms( +# cd_nom=data["cd_nom"], +# cd_ref=data["cd_ref"], +# nom_francais=data["nom_francais"] if "nom_francais" in data else None, +# comments=data["comments"] if "comments" in data else None, +# ) +# action = "INSERT" +# message = "Taxon ajouté" + +# db.session.add(bibTaxon) +# db.session.commit() + +# id_nom = bibTaxon.id_nom + +# # ###--------------Traitement des attibuts----------------- +# # Suppression des attributs existants +# for bibTaxonAtt in bibTaxon.attributs: +# db.session.delete(bibTaxonAtt) + +# db.session.flush() + +# if "attributs_values" in data: +# for att in data["attributs_values"]: +# if data["attributs_values"][att] != "" and data["attributs_values"][att] is not None: +# attVal = CorTaxonAttribut( +# id_attribut=att, +# cd_ref=bibTaxon.cd_ref, +# valeur_attribut=data["attributs_values"][att], +# ) +# db.session.add(attVal) + +# db.session.commit() + +# # ###--------------Traitement des listes----------------- +# # Suppression des listes existantes +# for bibTaxonLst in bibTaxon.listes: +# db.session.delete(bibTaxonLst) + +# db.session.flush() + +# if "listes" in data: +# for lst in data["listes"]: +# listTax = CorNomListe(id_liste=lst["id_liste"], id_nom=id_nom) +# db.session.add(listTax) + +# # Log +# logmanager.log_action(id_role, "bib_nom", id_nom, repr(bibTaxon), action, message) +# db.session.commit() +# return ( +# json.dumps({"success": True, "id_nom": id_nom}), +# 200, +# {"ContentType": "application/json"}, +# ) + + +# @adresses.route("/", methods=["DELETE"]) +# @fnauth.check_auth(6, True) +# @json_resp +# def delete_bibtaxons(id_nom, id_role=None): +# bibTaxon = db.session.query(BibNoms).filter_by(id_nom=id_nom).first() +# db.session.delete(bibTaxon) +# db.session.commit() + +# # #Log +# logmanager.log_action(id_role, "bib_nom", id_nom, repr(bibTaxon), "DELETE", "nom supprimé") + +# return bibTaxon.as_dict() + + +# # Private functions +# def getBibTaxonSynonymes(id_nom, cd_nom): +# q = ( +# db.session.query(BibNoms.id_nom) +# .join(BibNoms.taxref) +# .filter(Taxref.cd_ref == func.taxonomie.find_cdref(cd_nom)) +# .filter(BibNoms.id_nom != id_nom) +# ) +# results = q.all() +# return (q.count(), results) diff --git a/apptax/taxonomie/routestaxref.py b/apptax/taxonomie/routestaxref.py index 406ddc605..50c8e6f05 100644 --- a/apptax/taxonomie/routestaxref.py +++ b/apptax/taxonomie/routestaxref.py @@ -8,7 +8,6 @@ from ..utils.utilssqlalchemy import json_resp, serializeQuery, serializeQueryOneResult from .models import ( Taxref, - BibNoms, VMTaxrefListForautocomplete, BibTaxrefHabitats, BibTaxrefRangs, @@ -53,10 +52,11 @@ def getTaxrefVersion(): return taxref_version.as_dict() -@adresses.route("/bibnoms/", methods=["GET"]) -@json_resp -def getTaxrefBibtaxonList(): - return genericTaxrefList(True, request.args) +# @DEL_BIB_NOM +# @adresses.route("/bibnoms/", methods=["GET"]) +# @json_resp +# def getTaxrefBibtaxonList(): +# return genericTaxrefList(True, request.args) @adresses.route("/search//", methods=["GET"]) @@ -109,8 +109,10 @@ def getSearchInField(field, ilike): msg = f"No column found in Taxref for {field}" return jsonify(msg), 500 - if request.args.get("is_inbibnoms"): - q = q.join(BibNoms, BibNoms.cd_nom == Taxref.cd_nom) + # @DEL_BIB_NOM Paramètre obsolètes avec la suppression de bib_nom + # if request.args.get("is_inbibnoms"): + # q = q.join(BibNoms, BibNoms.cd_nom == Taxref.cd_nom) + join_on_bib_rang = False if request.args.get("add_rank"): q = q.join(BibTaxrefRangs, Taxref.id_rang == BibTaxrefRangs.id_rang) @@ -235,21 +237,23 @@ def getTaxrefHierarchieBibNoms(rang): def genericTaxrefList(inBibtaxon, parameters): - q = Taxref.query.options(raiseload("*"), joinedload(Taxref.bib_nom).joinedload(BibNoms.listes)) + taxrefColumns = Taxref.__table__.columns + # @DEL_BIB_NOM + # bibNomsColumns = BibNoms.__table__.columns + + q = db.session.query(Taxref) + + # @DEL_BIB_NOM + # qcount = q.outerjoin(BibNoms, BibNoms.cd_nom == Taxref.cd_nom) nbResultsWithoutFilter = q.count() - id_liste = request.args.get("id_liste", type=str, default=[]) - if id_liste and id_liste != "-1": - id_liste = id_liste.split(",") - filter_cor_nom_liste = aliased(CorNomListe) - filter_bib_noms = aliased(BibNoms) - q = q.join(filter_bib_noms, filter_bib_noms.cd_nom == Taxref.cd_nom) - q = q.join(filter_cor_nom_liste, filter_bib_noms.id_nom == filter_cor_nom_liste.id_nom) - q = q.filter(filter_cor_nom_liste.id_liste.in_(tuple(id_liste))) - if inBibtaxon is True: - q = q.filter(BibNoms.cd_nom.isnot(None)) + # @DEL_BIB_NOM + # if inBibtaxon is True: + # q = q.join(BibNoms, BibNoms.cd_nom == Taxref.cd_nom) + # else: + # q = q.outerjoin(BibNoms, BibNoms.cd_nom == Taxref.cd_nom) # Traitement des parametres limit = parameters.get("limit", 20, int) @@ -263,8 +267,9 @@ def genericTaxrefList(inBibtaxon, parameters): q = q.filter(Taxref.cd_nom == Taxref.cd_ref) elif param == "ilike": q = q.filter(Taxref.lb_nom.ilike(parameters[param] + "%")) - elif param == "is_inbibtaxons" and parameters[param] == "true": - q = q.filter(BibNoms.cd_nom.isnot(None)) + # @DEL_BIB_NOM + # elif param == "is_inbibtaxons" and parameters[param] == "true": + # q = q.filter(bibNomsColumns.cd_nom.isnot(None)) elif param.split("-")[0] == "ilike": value = unquote(parameters[param]) column = str(param.split("-")[1]) @@ -311,7 +316,7 @@ def genericTaxrefList(inBibtaxon, parameters): items.append(data) return { - "items": items, + "items": [d.as_dict() for d in results.items], "total": nbResultsWithoutFilter, "total_filtered": nbResults, "limit": limit, @@ -395,14 +400,41 @@ def get_AllTaxrefNameByListe(id_liste=None): - offset: numéro de la page """ # Traitement des cas ou code_liste = -1 - if id_liste == -1: - id_liste = None + id_liste = None + try: + if code_liste: + code_liste_to_int = int(code_liste) + if code_liste_to_int == -1: + id_liste = -1 + else: + id_liste = -1 + except ValueError: + # le code liste n'est pas un entier + # mais une chaine de caractère c-a-d bien un code + pass + + # Get id_liste + try: + # S'il y a un id_liste elle a forcement la valeur -1 + # c-a-d pas de liste + if not id_liste: + q = ( + db.session.query(BibListes.id_liste).filter(BibListes.code_liste == code_liste) + ).one() + id_liste = q[0] + except NoResultFound: + return ( + {"success": False, "message": "Code liste '{}' inexistant".format(code_liste)}, + 400, + ) + + # @DEL_BIB_NOM q = db.session.query(VMTaxrefListForautocomplete) - if id_liste: - q = q.join(BibNoms, BibNoms.cd_nom == VMTaxrefListForautocomplete.cd_nom).join( + if id_liste and id_liste != -1: + q = q.join( CorNomListe, - and_(CorNomListe.id_nom == BibNoms.id_nom, CorNomListe.id_liste == id_liste), + and_(CorNomListe.cd_nom == VMTaxrefListForautocomplete.cd_nom, CorNomListe.id_liste == id_liste), ) elif request.args.get("code_liste"): q = ( diff --git a/apptax/tests/fixtures.py b/apptax/tests/fixtures.py index 6eefe5b9b..50bdfb07e 100644 --- a/apptax/tests/fixtures.py +++ b/apptax/tests/fixtures.py @@ -1,8 +1,8 @@ import pytest from apptax.database import db -from apptax.taxonomie.models import BibListes, BibNoms, BibThemes, BibAttributs, CorTaxonAttribut - +from apptax.taxonomie.models import BibListes, BibThemes, BibAttributs, CorTaxonAttribut +, Taxref bibnom_exemple = [ (67111, 67111, "Ablette", None, "migrateur"), @@ -17,6 +17,28 @@ ] +@pytest.fixture +def noms_example(): + liste = BibListes.query.filter_by(code_liste="100").one() + with db.session.begin_nested(): + for cd_nom, cd_ref, nom_francais, comments in bibnom_exemple: + nom = Taxref.query.one( + cd_nom + ) + db.session.add(nom) + liste.noms.append(nom) + + +@pytest.fixture +def noms_without_listexample(): + with db.session.begin_nested(): + for cd_nom, cd_ref, nom_francais, comments in bibnom_exemple: + nom = BibNoms( + cd_nom=cd_nom, cd_ref=cd_ref, nom_francais=nom_francais, comments=comments + ) + db.session.add(nom) + + @pytest.fixture def attribut_example(): theme = BibThemes.query.filter_by(nom_theme="Mon territoire").one() diff --git a/apptax/tests/test_taxref.py b/apptax/tests/test_taxref.py index 3341bc4e5..7e249ccb2 100644 --- a/apptax/tests/test_taxref.py +++ b/apptax/tests/test_taxref.py @@ -230,10 +230,12 @@ def test_searchTaxref_routes(self): query_string = {"ilike-classe": "hex", "page": 1, "limit": 10} response = self.client.get(url_for("taxref.getTaxrefList"), query_string=query_string) assert response.status_code == 200 - response = self.client.get( - url_for("taxref.getTaxrefBibtaxonList"), query_string=query_string - ) - assert response.status_code == 200 + + # @DEL_BIB_NOM + # response = self.client.get( + # url_for("taxref.getTaxrefBibtaxonList"), query_string=query_string + # ) + # assert response.status_code == 200 def test_regneGroup2Inpn_routes(self): response = self.client.get(url_for("taxref.get_regneGroup2Inpn_taxref")) From 901cbd3f9410a2db91992fcc8d1bdb74cd48442a Mon Sep 17 00:00:00 2001 From: amandine-sahl Date: Tue, 22 Nov 2022 18:46:43 +0100 Subject: [PATCH 02/25] Feat taxhub v2 : Interface graphique flask-admin --- .gitignore | 3 + README.rst | 6 +- apptax/__init__.py | 9 + apptax/admin/admin.py | 89 +++ apptax/admin/admin_view.py | 625 ++++++++++++++++++ apptax/admin/filters.py | 103 +++ .../admin/static}/images/pictos/amphibien.gif | Bin .../admin/static}/images/pictos/araignee.gif | Bin .../static}/images/pictos/champignon.gif | Bin .../admin/static}/images/pictos/ecrevisse.gif | Bin .../admin/static}/images/pictos/favicon.png | Bin .../admin/static}/images/pictos/insecte.gif | Bin .../admin/static}/images/pictos/mammifere.gif | Bin .../admin/static}/images/pictos/mollusque.gif | Bin .../admin/static}/images/pictos/mousse.gif | Bin .../admin/static}/images/pictos/nopicto.gif | Bin .../admin/static}/images/pictos/oiseau.gif | Bin .../images/pictos/picto_Angiospermes.png | Bin .../static}/images/pictos/picto_Annelides.png | Bin .../static}/images/pictos/picto_Bivalves.png | Bin .../static}/images/pictos/picto_Fougeres.png | Bin .../images/pictos/picto_Gasteropodes.png | Bin .../images/pictos/picto_Gymnospermes.png | Bin .../images/pictos/picto_Mammiferes.png | Bin .../images/pictos/picto_Myriapodes.png | Bin .../admin/static}/images/pictos/plante.gif | Bin .../admin/static}/images/pictos/poisson.gif | Bin .../admin/static}/images/pictos/reptile.gif | Bin .../admin/static}/images/pictos/squelette.png | Bin apptax/admin/static/js/login.js | 33 + apptax/admin/static/js/regne_group2_inpn.js | 31 + apptax/admin/static/js/taxref_autocomplete.js | 27 + .../templates/admin/custom_row_actions.html | 15 + .../admin/templates/admin/details_taxref.html | 199 ++++++ apptax/admin/templates/admin/edit_taxref.html | 134 ++++ .../admin/templates/admin/list_biblist.html | 5 + apptax/admin/templates/admin/list_taxref.html | 105 +++ apptax/admin/templates/admin/login.html | 18 + .../templates/admin/populate_biblist.html | 44 ++ apptax/admin/utils.py | 73 ++ apptax/app.py | 82 +-- apptax/config.py.sample | 15 +- apptax/database.py | 1 - apptax/index.py | 8 - apptax/log/__init__.py | 1 - apptax/log/logmanager.py | 19 - apptax/log/models.py | 20 - ...4f9b_delete_attributesviews_per_kingdom.py | 92 +++ ...d1b5dd965e_strip_static_media_from_path.py | 44 ++ .../versions/5cef20a05a20_remove_picto.py | 28 + .../633e0ad4c4e3_save_bib_nom_data.py | 48 ++ ...d64c7_delete_bib_noms_id_nom_dependancy.py | 68 -- .../a982df406ae8_remove_t_admin_log.py | 35 + ...734f490ff_delete_bib_noms_cor_nom_liste.py | 64 -- ...d734f490ff_delete_bib_noms_dependancies.py | 202 ++++++ ...8be_change_tmedia_before_insert_trigger.py | 19 +- .../versions/f6abb7857493_delete_bib_noms.py | 50 ++ {static => apptax/static}/favicon.ico | Bin apptax/static/logo_inpn.png | Bin 0 -> 60966 bytes apptax/taxonomie/filemanager.py | 245 +------ apptax/taxonomie/models.py | 268 +++++--- apptax/taxonomie/repositories.py | 154 ----- apptax/taxonomie/routesbdcstatuts.py | 6 +- apptax/taxonomie/routesbibattributs.py | 48 +- apptax/taxonomie/routesbiblistes.py | 256 +------ apptax/taxonomie/routesbibnoms.py | 238 +------ apptax/taxonomie/routesbibtypesmedia.py | 23 - apptax/taxonomie/routestaxref.py | 233 ++----- apptax/taxonomie/routestmedias.py | 129 +--- apptax/taxonomie/schemas.py | 87 +++ apptax/templates/admin/master.html | 117 ++++ apptax/templates/index.html | 130 ---- apptax/test_config.py | 2 +- apptax/tests/conftest.py | 1 + apptax/tests/fixtures.py | 131 +++- apptax/tests/test_admin.py | 200 ++++++ apptax/tests/test_bib_noms.py | 2 +- apptax/tests/test_biblistes.py | 64 ++ apptax/tests/test_media.py | 104 +-- apptax/tests/test_taxhub.py | 53 -- apptax/tests/test_taxref.py | 92 +-- apptax/utils/genericfunctions.py | 24 - apptax/utils/routesconfig.py | 27 - apptax/utils/utilssqlalchemy.py | 110 --- docs/changelog.md | 11 + docs/installation.rst | 8 +- docs/manuel-administrateur.rst | 13 + install_app.sh | 17 - requirements-common.in | 4 +- requirements-dev.txt | 18 +- requirements.txt | 24 +- setup.py | 4 +- static/.gitignore | 2 - static/.nvmrc | 1 - static/app/app.js | 131 ---- .../detail/bibliste-detail-controler.js | 55 -- .../bib_liste/detail/bibliste-detail-tpl.html | 119 ---- .../bib_liste/edit/bibliste-edit-controler.js | 162 ----- .../app/bib_liste/edit/bibliste-edit-tpl.html | 102 --- .../bib_liste/list/bibliste-list-controler.js | 83 --- .../app/bib_liste/list/bibliste-list-tpl.html | 106 --- .../populate/bibliste-populate-controler.js | 254 ------- .../populate/bibliste-populate-tpl.html | 197 ------ .../bib_nom/detail/bibNom-detail-controler.js | 44 -- .../app/bib_nom/detail/bibNom-detail-tpl.html | 135 ---- .../app/bib_nom/edit/bibNom-form-controler.js | 159 ----- static/app/bib_nom/edit/bibNom-form-tpl.html | 101 --- .../media/createBibnomsMedias-directives.js | 171 ----- .../media/createBibnomsMedias-template.html | 173 ----- .../app/bib_nom/list/bibNom-list-controler.js | 150 ----- static/app/bib_nom/list/bibNom-list-tpl.html | 121 ---- .../createBibnomsAttributesForm-directives.js | 11 - .../createBibnomsAttributesForm-template.html | 79 --- .../createBibnomsListesForm-directives.js | 40 -- .../createBibnomsListesForm-template.html | 29 - .../directives/createTooltip-directives.js | 14 - ...teBibnomsAttributesFormInput-directives.js | 112 ---- .../form_input/input-checkbox-template.html | 6 - .../input-multiselect-template.html | 30 - .../form_input/input-phenology-template.html | 11 - .../directives/searchHierarchie-directive.js | 47 -- .../directives/searchHierarchie-template.html | 46 -- static/app/components/header-template.html | 34 - static/app/constants.js.sample | 8 - static/app/login/form.html | 25 - static/app/login/loginControler.js | 165 ----- static/app/login/loginlogout-template.html | 35 - .../app/taxref/detail/taxrefDetailModal.html | 72 -- .../taxref/detail/taxrefModalInfoControler.js | 24 - static/app/taxref/list/taxref.html | 155 ----- static/app/taxref/list/taxrefControler.js | 165 ----- static/images/spinner.gif | Bin 8942 -> 0 bytes static/medias/.gitignore | 2 - static/package-lock.json | 76 --- static/package.json | 41 -- 135 files changed, 3091 insertions(+), 5560 deletions(-) create mode 100644 apptax/admin/admin.py create mode 100644 apptax/admin/admin_view.py create mode 100644 apptax/admin/filters.py rename {static => apptax/admin/static}/images/pictos/amphibien.gif (100%) rename {static => apptax/admin/static}/images/pictos/araignee.gif (100%) rename {static => apptax/admin/static}/images/pictos/champignon.gif (100%) rename {static => apptax/admin/static}/images/pictos/ecrevisse.gif (100%) rename {static => apptax/admin/static}/images/pictos/favicon.png (100%) rename {static => apptax/admin/static}/images/pictos/insecte.gif (100%) rename {static => apptax/admin/static}/images/pictos/mammifere.gif (100%) rename {static => apptax/admin/static}/images/pictos/mollusque.gif (100%) rename {static => apptax/admin/static}/images/pictos/mousse.gif (100%) rename {static => apptax/admin/static}/images/pictos/nopicto.gif (100%) rename {static => apptax/admin/static}/images/pictos/oiseau.gif (100%) rename {static => apptax/admin/static}/images/pictos/picto_Angiospermes.png (100%) rename {static => apptax/admin/static}/images/pictos/picto_Annelides.png (100%) rename {static => apptax/admin/static}/images/pictos/picto_Bivalves.png (100%) rename {static => apptax/admin/static}/images/pictos/picto_Fougeres.png (100%) rename {static => apptax/admin/static}/images/pictos/picto_Gasteropodes.png (100%) rename {static => apptax/admin/static}/images/pictos/picto_Gymnospermes.png (100%) rename {static => apptax/admin/static}/images/pictos/picto_Mammiferes.png (100%) rename {static => apptax/admin/static}/images/pictos/picto_Myriapodes.png (100%) rename {static => apptax/admin/static}/images/pictos/plante.gif (100%) rename {static => apptax/admin/static}/images/pictos/poisson.gif (100%) rename {static => apptax/admin/static}/images/pictos/reptile.gif (100%) rename {static => apptax/admin/static}/images/pictos/squelette.png (100%) create mode 100644 apptax/admin/static/js/login.js create mode 100644 apptax/admin/static/js/regne_group2_inpn.js create mode 100644 apptax/admin/static/js/taxref_autocomplete.js create mode 100644 apptax/admin/templates/admin/custom_row_actions.html create mode 100644 apptax/admin/templates/admin/details_taxref.html create mode 100644 apptax/admin/templates/admin/edit_taxref.html create mode 100644 apptax/admin/templates/admin/list_biblist.html create mode 100644 apptax/admin/templates/admin/list_taxref.html create mode 100644 apptax/admin/templates/admin/login.html create mode 100644 apptax/admin/templates/admin/populate_biblist.html create mode 100644 apptax/admin/utils.py delete mode 100644 apptax/index.py delete mode 100644 apptax/log/__init__.py delete mode 100644 apptax/log/models.py create mode 100644 apptax/migrations/versions/1cf2cdc94f9b_delete_attributesviews_per_kingdom.py create mode 100644 apptax/migrations/versions/52d1b5dd965e_strip_static_media_from_path.py create mode 100644 apptax/migrations/versions/5cef20a05a20_remove_picto.py create mode 100644 apptax/migrations/versions/633e0ad4c4e3_save_bib_nom_data.py delete mode 100644 apptax/migrations/versions/73306d6d64c7_delete_bib_noms_id_nom_dependancy.py create mode 100644 apptax/migrations/versions/a982df406ae8_remove_t_admin_log.py delete mode 100644 apptax/migrations/versions/b7d734f490ff_delete_bib_noms_cor_nom_liste.py create mode 100644 apptax/migrations/versions/b7d734f490ff_delete_bib_noms_dependancies.py create mode 100644 apptax/migrations/versions/f6abb7857493_delete_bib_noms.py rename {static => apptax/static}/favicon.ico (100%) create mode 100644 apptax/static/logo_inpn.png delete mode 100644 apptax/taxonomie/routesbibtypesmedia.py create mode 100644 apptax/taxonomie/schemas.py create mode 100644 apptax/templates/admin/master.html create mode 100644 apptax/tests/test_admin.py create mode 100644 apptax/tests/test_biblistes.py delete mode 100644 apptax/tests/test_taxhub.py delete mode 100644 apptax/utils/genericfunctions.py delete mode 100644 apptax/utils/routesconfig.py delete mode 100644 static/.gitignore delete mode 100644 static/.nvmrc delete mode 100644 static/app/app.js delete mode 100644 static/app/bib_liste/detail/bibliste-detail-controler.js delete mode 100644 static/app/bib_liste/detail/bibliste-detail-tpl.html delete mode 100644 static/app/bib_liste/edit/bibliste-edit-controler.js delete mode 100644 static/app/bib_liste/edit/bibliste-edit-tpl.html delete mode 100644 static/app/bib_liste/list/bibliste-list-controler.js delete mode 100644 static/app/bib_liste/list/bibliste-list-tpl.html delete mode 100644 static/app/bib_liste/populate/bibliste-populate-controler.js delete mode 100644 static/app/bib_liste/populate/bibliste-populate-tpl.html delete mode 100644 static/app/bib_nom/detail/bibNom-detail-controler.js delete mode 100644 static/app/bib_nom/detail/bibNom-detail-tpl.html delete mode 100644 static/app/bib_nom/edit/bibNom-form-controler.js delete mode 100644 static/app/bib_nom/edit/bibNom-form-tpl.html delete mode 100644 static/app/bib_nom/edit/media/createBibnomsMedias-directives.js delete mode 100644 static/app/bib_nom/edit/media/createBibnomsMedias-template.html delete mode 100644 static/app/bib_nom/list/bibNom-list-controler.js delete mode 100644 static/app/bib_nom/list/bibNom-list-tpl.html delete mode 100644 static/app/components/directives/createBibnomsAttributesForm-directives.js delete mode 100644 static/app/components/directives/createBibnomsAttributesForm-template.html delete mode 100644 static/app/components/directives/createBibnomsListesForm-directives.js delete mode 100644 static/app/components/directives/createBibnomsListesForm-template.html delete mode 100644 static/app/components/directives/createTooltip-directives.js delete mode 100644 static/app/components/directives/form_input/createBibnomsAttributesFormInput-directives.js delete mode 100644 static/app/components/directives/form_input/input-checkbox-template.html delete mode 100644 static/app/components/directives/form_input/input-multiselect-template.html delete mode 100644 static/app/components/directives/form_input/input-phenology-template.html delete mode 100644 static/app/components/directives/searchHierarchie-directive.js delete mode 100644 static/app/components/directives/searchHierarchie-template.html delete mode 100644 static/app/components/header-template.html delete mode 100644 static/app/constants.js.sample delete mode 100644 static/app/login/form.html delete mode 100644 static/app/login/loginControler.js delete mode 100644 static/app/login/loginlogout-template.html delete mode 100644 static/app/taxref/detail/taxrefDetailModal.html delete mode 100644 static/app/taxref/detail/taxrefModalInfoControler.js delete mode 100644 static/app/taxref/list/taxref.html delete mode 100644 static/app/taxref/list/taxrefControler.js delete mode 100644 static/images/spinner.gif delete mode 100755 static/medias/.gitignore delete mode 100644 static/package-lock.json delete mode 100644 static/package.json diff --git a/.gitignore b/.gitignore index eb846c9ab..24a5d4c99 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,8 @@ *~ +static/medias/* + +node_modules #taxhub app settings.ini venv diff --git a/README.rst b/README.rst index 0459466c1..7bf120365 100644 --- a/README.rst +++ b/README.rst @@ -50,10 +50,8 @@ Elle est centralisée dans l'application `UsersHub `_ -* 4 = Idem 3 + Possibilité d'ajouter des taxons dans ``bib_noms``, de les mettre dans des listes et de renseigner tous leurs attributs (notamment ceux utilisés par `GeoNature `_) -* 6 = Administrateurs +* 2 = Gestion des taxons (ajout/modification/suppression des attributs, liste et médias sur les taxons) +* 6 = Administrateurs: création de liste, attributs et thême Auteurs ------- diff --git a/apptax/__init__.py b/apptax/__init__.py index e69de29bb..d51b47064 100644 --- a/apptax/__init__.py +++ b/apptax/__init__.py @@ -0,0 +1,9 @@ +taxhub_routes = [ + ("apptax.taxonomie.routesbibnoms:adresses", "/api/bibnoms"), + ("apptax.taxonomie.routestaxref:adresses", "/api/taxref"), + ("apptax.taxonomie.routesbibattributs:adresses", "/api/bibattributs"), + ("apptax.taxonomie.routesbiblistes:adresses", "/api/biblistes"), + ("apptax.taxonomie.routestmedias:adresses", "/api/tmedias"), + ("apptax.taxonomie.routesbdcstatuts:adresses", "/api/bdc_statuts"), + ("apptax.admin.admin:adresses", "/"), +] diff --git a/apptax/admin/admin.py b/apptax/admin/admin.py new file mode 100644 index 000000000..54c845e89 --- /dev/null +++ b/apptax/admin/admin.py @@ -0,0 +1,89 @@ +import os + +from flask import redirect, url_for, Blueprint +from flask_admin import Admin, AdminIndexView, expose +from werkzeug.exceptions import Unauthorized + +from apptax.database import db +from apptax.taxonomie.models import Taxref, BibListes, TMedias, BibAttributs, BibThemes + + +# Create blueprint for template and static +adresses = Blueprint("apptax-admin", __name__, template_folder="templates") +adresses.static_folder = os.path.join(adresses.root_path, "static") + + +class TaxhubView(AdminIndexView): + def is_visible(self): + # This view won't appear in the menu structure + return False + + @expose("/") + def index(self): + return redirect(url_for("taxons.index_view")) + + +taxhub_admin = Admin( + template_mode="bootstrap4", name="Administration Taxhub", index_view=TaxhubView(url="/") +) + + +def taxhub_admin_addview(app, admin, category=None): + with app.app_context(): + from apptax.admin.admin_view import ( + TaxrefView, + BibListesView, + TMediasView, + BibAttributsView, + LoginView, + BibThemesView, + ) + + static_folder = os.path.join(adresses.root_path, "static") + admin.add_view( + LoginView( + name="Login", endpoint="loginview", category=category, static_folder=static_folder + ) + ) + admin.add_view( + TaxrefView( + Taxref, + db.session, + name="Taxref", + endpoint="taxons", + category=category, + static_folder=static_folder, + ) + ) + admin.add_view( + BibListesView( + BibListes, + db.session, + name="Listes", + category=category, + static_folder=static_folder, + ) + ) + admin.add_view( + TMediasView( + TMedias, db.session, name="Médias", category=category, static_folder=static_folder + ) + ) + admin.add_view( + BibAttributsView( + BibAttributs, + db.session, + name="Attributs", + category=category, + static_folder=static_folder, + ) + ) + admin.add_view( + BibThemesView( + BibThemes, + db.session, + name="Thèmes", + category=category, + static_folder=static_folder, + ) + ) diff --git a/apptax/admin/admin_view.py b/apptax/admin/admin_view.py new file mode 100644 index 000000000..fcd87a126 --- /dev/null +++ b/apptax/admin/admin_view.py @@ -0,0 +1,625 @@ +import os +import csv +import logging +import logging + +from pathlib import Path + +from flask import request, json, url_for, redirect, flash, g, current_app +from flask_admin.model.template import macro +from jinja2.utils import markupsafe + + +from flask_admin import form, BaseView +from flask_admin.contrib.sqla import ModelView +from flask_admin.model.form import InlineFormAdmin +from flask_admin.model.ajax import AjaxModelLoader, DEFAULT_PAGE_SIZE +from flask_admin.contrib.sqla.filters import FilterEqual + + +from flask_admin.base import expose +from flask_admin.model.helpers import get_mdict_item_or_list + +from flask_admin.form.upload import FileUploadField + +from flask_admin.model.template import EndpointLinkRowAction, TemplateLinkRowAction +from flask_admin.model.template import EndpointLinkRowAction, TemplateLinkRowAction + +from sqlalchemy import or_, inspect + +from sqlalchemy.orm import joinedload +from sqlalchemy.orm import undefer + +from wtforms import Form, BooleanField, SelectField, PasswordField, StringField + +from wtforms.validators import ValidationError, DataRequired, Length + +from apptax.database import db +from apptax.taxonomie.models import ( + BibThemes, + Taxref, + BibAttributs, + CorTaxonAttribut, + TMedias, + BibListes, + cor_nom_liste, +) +from apptax.admin.utils import taxref_media_file_name, get_user_permission +from pypnusershub.utils import get_current_app_id +from apptax.admin.admin import adresses +from apptax.admin.utils import PopulateBibListeException, populate_bib_liste +from apptax.admin.filters import ( + TaxrefDistinctFilter, + FilterTaxrefAttr, + FilterBiblist, + FilterIsValidName, + FilterMedia, + FilterAttributes, +) + +log = logging.getLogger(__name__) + +log = logging.getLogger(__name__) + + +class FlaskAdminProtectedMixin: + # Define permission level for extra actions + # Dict : { + # ".action_endpoint" : level + # } + extra_actions_perm = None + + def _can_action(self, level): + if not g.current_user: + return False + if not g.current_user.is_authenticated: + return False + user_perm = get_user_permission(g.current_user.id_role) + if not user_perm: + return False + return user_perm.id_droit_max >= level + + def get_list_row_actions(self): + """ + Test permission on extra row action + """ + actions = super().get_list_row_actions() + if not self.extra_actions_perm: + return actions + + for extra_action_perm in self.extra_actions_perm: + actions = self._can_extra_action( + actions=actions, + extra_action_name=extra_action_perm, + extra_action_level=self.extra_actions_perm[extra_action_perm], + ) + + return actions + + def _can_extra_action(self, actions, extra_action_name, extra_action_level): + for id, extra_action in enumerate(actions): + if extra_action in self.column_extra_row_actions: + if ( + getattr(extra_action, "endpoint", None) + or getattr(extra_action, "template", None) + ) == extra_action_name and not self._can_action(extra_action_level): + actions.pop(id) + return actions + + @property + def can_export(self): + return self._can_action(0) + + +class LoginForm(Form): + identifiant = StringField("identifiant", validators=[DataRequired(), Length(1, 64)]) + password = PasswordField("Password", validators=[DataRequired()]) + + +class LoginView(BaseView): + def is_visible(self): + # Hide view in navbar + return False + + @expose("/", methods=("GET", "POST")) + def index(self): + form = LoginForm(request.form) + return self.render("admin/login.html", form=form) + + def render(self, template, **kwargs): + self.extra_js = [url_for(".static", filename="js/login.js")] + self._template_args["RETURN_URL"] = get_mdict_item_or_list(request.args, "redirect") + self._template_args["IP_APP"] = get_current_app_id() + return super(LoginView, self).render(template, **kwargs) + + +class PopulateBibListesForm(Form): + delimiter = SelectField(label="Delimiter", choices=[(",", ","), (";", ";")]) + with_header = BooleanField(label="With header") + upload = FileUploadField(label="File", allowed_extensions=("csv",)) + + +class BibThemesView( + FlaskAdminProtectedMixin, + ModelView, +): + @property + def can_create(self): + return self._can_action(6) + + @property + def can_edit(self): + return self._can_action(6) + + @property + def can_delete(self): + return self._can_action(6) + + extra_actions_perm = None + form_excluded_columns = ["attributs"] + + +class BibListesView(FlaskAdminProtectedMixin, ModelView): + @property + def can_create(self): + return self._can_action(6) + + @property + def can_edit(self): + return self._can_action(6) + + @property + def can_delete(self): + return self._can_action(6) + + extra_actions_perm = {".import_cd_nom_view": 6} + list_template = "admin/list_biblist.html" + extra_actions_perm = {".import_cd_nom_view": 6, "custom_row_actions.truncate_bib_liste": 6} + + can_view_details = True + + column_list = ("regne", "group2_inpn", "code_liste", "nom_liste", "nb_taxons") + + column_labels = dict(nb_taxons="Nb taxons") + + form_excluded_columns = "noms" + + column_extra_row_actions = [ + EndpointLinkRowAction("fa fa-download", ".import_cd_nom_view", "Peupler liste"), + TemplateLinkRowAction("custom_row_actions.truncate_bib_liste", "Effacer cd_nom liste"), + EndpointLinkRowAction("fa fa-download", ".import_cd_nom_view", "Peupler liste"), + TemplateLinkRowAction("custom_row_actions.truncate_bib_liste", "Effacer cd_nom liste"), + ] + + def on_model_change(self, form, model, is_created): + """ + Force None on empty string regne + """ + if model.regne and not model.regne.regne: + model.regne = None + + def render(self, template, **kwargs): + self.extra_js = [ + url_for( + "configs.get_config", + variable_name="URL_GROUP_REGNE", + str_endpoint="taxref.get_regneGroup2Inpn_taxref", + ), + url_for(".static", filename="js/regne_group2_inpn.js"), + ] + + return super(BibListesView, self).render(template, **kwargs) + + def delete_model(self, model): + """ + Delete model test if cd_nom in list + """ + nb_noms = ( + db.session.query(cor_nom_liste) + .filter(cor_nom_liste.c.id_liste == model.id_liste) + .count() + ) + if nb_noms > 0: + flash( + f"Impossible de supprimer la liste {model.nom_liste} car il y a des noms associés", + "error", + ) + return False + else: + super().delete_model(model) + + @expose("/truncate_bib_liste", methods=("POST",)) + def truncate_bib_liste(self): + """ + Suppression des cd_noms contenus dans la liste + """ + try: + id = request.form.get("id") + liste = BibListes.query.get(id) + if liste.noms: + liste.noms = [] + db.session.add(liste) + db.session.commit() + flash("Liste purgée de ses noms") + return redirect(self.get_url(".index_view")) + except Exception as ex: + log.error(str(ex)) + flash("Erreur, liste non purgée", "error") + return redirect(self.get_url(".index_view")) + + @expose("/import_cd_nom/", methods=("GET", "POST")) + def import_cd_nom_view(self, *args, **kwargs): + form = PopulateBibListesForm(request.form) + + if request.method == "POST": + id_list = get_mdict_item_or_list(request.args, "id") + delimiter = request.form.get("delimiter", default=",") + with_header = request.form.get("with_header", default=False) + file = request.files["upload"] + + try: + populate_bib_liste(id_list, delimiter, with_header, file) + except PopulateBibListeException as e: + flash(e.message, "error") + return self.render("admin/populate_biblist.html", form=form) + + return redirect(self.get_url(".index_view")) + + return self.render("admin/populate_biblist.html", form=form) + + +class InlineMediaForm(InlineFormAdmin): + form_label = "Médias" + form_extra_fields = { + "chemin": form.ImageUploadField( + label="Image", + base_path=Path(current_app.config["MEDIA_FOLDER"], "taxhub").absolute(), + namegen=taxref_media_file_name, + ) + } + + def __init__(self): + return super(InlineMediaForm, self).__init__(TMedias) + + def on_model_change(self, form, model, is_created): + """ + Check if chemin or url is set + """ + if not model.chemin and not model.url: + raise ValidationError(f"Média {model.titre} fichier ou url obligatoire") + + +class TaxrefView( + FlaskAdminProtectedMixin, + ModelView, +): + can_create = False + can_export = False + can_delete = False + can_view_details = True + + @property + def can_edit(self): + return self._can_action(2) + + inline_models = (InlineMediaForm(),) + + # Exclude all fields except listes + form_excluded_columns = [c[0] for c in inspect(Taxref).attrs.items() if not c[0] == "listes"] + + column_list = ( + "cd_nom", + "cd_ref", + "regne", + "group2_inpn", + "nom_complet", + "nom_valide", + "nom_vern", + "classe", + "ordre", + "famille", + "listes", + "nb_attributs", + "nb_medias", + ) + + def _apply_search(self, query, count_query, joins, count_joins, search): + """ + Apply search to the autocomplete query + """ + query = query.filter(Taxref.cd_nom == int(search)) + count_query = count_query.filter(Taxref.cd_nom == int(search)) + return query, count_query, joins, count_joins + + column_searchable_list = ["nom_complet", "cd_nom"] + + column_filters = [ + FilterEqual(Taxref.cd_nom, name="cd_nom"), + FilterEqual(Taxref.cd_ref, name="cd_ref"), + TaxrefDistinctFilter(column=Taxref.regne, name="Règne"), + TaxrefDistinctFilter(column=Taxref.group2_inpn, name="Group2 INPN"), + TaxrefDistinctFilter(column=Taxref.classe, name="Classe"), + FilterBiblist( + column="listes", + name="Est dans la liste", + ), + FilterTaxrefAttr( + column="attributs", + name="A l'attribut", + ), + FilterIsValidName( + name="Nom valide / synonyme", options=[(1, "Nom valide"), (0, "Synonyme")] + ), + FilterMedia( + name="Média", options=[(1, "Possède un média"), (0, "Ne possède pas de média")] + ), + FilterAttributes( + name="Attributs", + options=[(1, "Possède un attribut"), (0, "Ne possède pas d'attribut")], + ), + ] + column_formatters = {c: macro("render_nom_ref") for c in column_list} + + column_auto_select_related = True + column_hide_backrefs = False + + list_template = "admin/list_taxref.html" + edit_template = "admin/edit_taxref.html" + details_template = "admin/details_taxref.html" + + def _get_theme_attributes(self, taxon): + return ( + db.session.query(BibThemes) + .filter(or_(BibAttributs.v_regne == taxon.regne, BibAttributs.v_regne == None)) + .filter( + or_( + BibAttributs.v_group2_inpn == taxon.group2_inpn, + BibAttributs.v_group2_inpn == None, + ) + ) + .order_by(BibAttributs.ordre) + .all() + ) + + def get_query(self): + return self.session.query(self.model).options( + undefer("nb_attributs"), undefer("nb_medias") + ) + + def _get_attributes_value(self, taxon_name, theme_attributs_def): + attributes_val = {} + for a in [a for attrs in [t.attributs for t in theme_attributs_def] for a in attrs]: + # Désérialisation du champ liste_valeur_attribut + attributes_val[a.id_attribut] = json.loads(a.liste_valeur_attribut) + # Ajout des valeurs du taxon si elle existe + taxon_att = [ + tatt for tatt in taxon_name.attributs if tatt.id_attribut == a.id_attribut + ] + if taxon_att: + attributes_val[a.id_attribut]["taxon_attr_value"] = taxon_att[0].valeur_attribut + return attributes_val + + def render(self, template, **kwargs): + if template == "admin/list_taxref.html": + self.extra_js = [ + url_for(".static", filename="js/taxref_autocomplete.js"), + ] + + return super(TaxrefView, self).render(template, **kwargs) + + @expose("/details/", methods=("GET",)) + def details_view(self): + id = get_mdict_item_or_list(request.args, "id") + taxon_name = db.session.query(Taxref).get(id) + if not taxon_name.cd_nom == taxon_name.cd_ref: + taxon_valid = db.session.query(Taxref).get(taxon_name.cd_ref) + else: + taxon_valid = taxon_name + + # Get attributes + theme_attributs_def = self._get_theme_attributes(taxon_valid) + attributes_val = self._get_attributes_value(taxon_valid, theme_attributs_def) + self._template_args["theme_attributs_def"] = theme_attributs_def + self._template_args["attributes_val"] = attributes_val + + # Get media + self._template_args["medias"] = taxon_valid.medias + return super(TaxrefView, self).details_view() + + @expose("/edit/", methods=("GET", "POST")) + def edit_view(self): + # Get Taxon data + id = get_mdict_item_or_list(request.args, "id") + taxon_name = db.session.query(Taxref).get(id) + + # Get attributes only if cd_nom is cd_ref + if taxon_name.cd_nom == taxon_name.cd_ref: + theme_attributs_def = self._get_theme_attributes(taxon_name) + attributes_val = self._get_attributes_value(taxon_name, theme_attributs_def) + self._template_args["theme_attributs_def"] = theme_attributs_def + self._template_args["attributes_val"] = attributes_val + if request.method == "POST": + for f in request.form: + if request.form.getlist(f) and f.startswith("attr."): + id_attr = f.split(".")[1] + value = "&".join(request.form.getlist(f)) + query = ( + db.select(CorTaxonAttribut) + .filter_by(cd_ref=taxon_name.cd_ref) + .filter_by(id_attribut=id_attr) + ) + model = db.session.scalars(query).one_or_none() + if model: + if value == "": + db.session.delete(model) + else: + model.valeur_attribut = value + db.session.add(model) + elif value != "": + model = CorTaxonAttribut( + cd_ref=taxon_name.cd_ref, + id_attribut=id_attr, + valeur_attribut=value, + ) + db.session.add(model) + db.session.commit() + self._template_args["url_cancel"] = request.referrer or url_for("taxons.index_view") + + return super(TaxrefView, self).edit_view() + + +class TaxrefAjaxModelLoader(AjaxModelLoader): + def __init__(self, name, **options): + super(TaxrefAjaxModelLoader, self).__init__(name, options) + + def format(self, model): + if model: + return (model.cd_ref, model.nom_complet) + return None + + def get_one(self, pk): + return Taxref.query.filter(Taxref.cd_nom == pk).first() + + def get_list(self, query, offset=0, limit=DEFAULT_PAGE_SIZE): + results = ( + Taxref.query.filter(Taxref.nom_complet.ilike(f"{query}%")) + .limit(limit) + .offset(offset) + .all() + ) + return results + + +class TMediasView(FlaskAdminProtectedMixin, ModelView): + can_create = False + + @property + def can_edit(self): + return self._can_action(2) + + @property + def can_delete(self): + return self._can_action(2) + + form_ajax_refs = {"taxon": TaxrefAjaxModelLoader("taxon")} + + column_exclude_list = ("url",) + form_extra_fields = { + "chemin": form.ImageUploadField( + label="Image", + base_path=current_app.config["MEDIA_FOLDER"], + url_relative_path="", + namegen=taxref_media_file_name, + thumbnail_size=(150, 150, True), + endpoint="media", + ) + } + + def _list_titre(view, context, model, name): + return markupsafe.Markup(model.titre) + + def _list_thumbnail(view, context, model, name): + # format html + html = "" + if model.types.nom_type_media in ("Photo", "Photo_principale"): + html = f""" + + Taxon image + + """ + elif model.types.nom_type_media in ("Audio"): + html = f"