Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: auto_validation function #2768

Merged
merged 14 commits into from
Jan 24, 2024
Merged
19 changes: 18 additions & 1 deletion backend/geonature/core/gn_commons/models/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from collections import defaultdict

from flask import current_app
from sqlalchemy import ForeignKey
from sqlalchemy import ForeignKey, text
from sqlalchemy.orm import relationship, aliased
from sqlalchemy.sql import select, func
from sqlalchemy.dialects.postgresql import UUID
Expand Down Expand Up @@ -233,6 +233,23 @@
overlaps="nomenclature_valid_status", # overlaps expected
)

@staticmethod
def auto_validation(fct_auto_validation):
stmt = text(
f"""
select routine_name, routine_schema
from information_schema.routines
where routine_name= '{fct_auto_validation}'
and routine_type='FUNCTION';
"""
)
result = DB.session.execute(stmt).fetchall()
if not result:
return

Check warning on line 248 in backend/geonature/core/gn_commons/models/base.py

View check run for this annotation

Codecov / codecov/patch

backend/geonature/core/gn_commons/models/base.py#L248

Added line #L248 was not covered by tests
stmt_auto_validation = text(f"SELECT gn_profiles.{fct_auto_validation}()")
DB.session.execute(stmt_auto_validation)
DB.session.commit()


last_validation_query = (
select(TValidations)
Expand Down
6 changes: 6 additions & 0 deletions backend/geonature/tests/fixtures.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@
"celery_eager",
"sources_modules",
"modules",
"auto_validation_enabled",
]


Expand Down Expand Up @@ -675,3 +676,8 @@ def create_report(id_synthese, id_role, content, id_type, deleted):
@pytest.fixture()
def notifications_enabled(monkeypatch):
monkeypatch.setitem(current_app.config, "NOTIFICATIONS_ENABLED", True)


@pytest.fixture()
def auto_validation_enabled(monkeypatch):
jacquesfize marked this conversation as resolved.
Show resolved Hide resolved
monkeypatch.setitem(current_app.config["VALIDATION"], "AUTO_VALIDATION_ENABLED", True)
76 changes: 75 additions & 1 deletion backend/geonature/tests/test_validation.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,50 @@
from werkzeug.exceptions import Unauthorized, BadRequest

from geonature.core.gn_synthese.models import Synthese
from geonature.core.gn_commons.models import TValidations, VLatestValidations
from geonature.core.gn_profiles.models import VConsistancyData
from gn_module_validation.tasks import set_auto_validation
from geonature.utils.env import db
from geonature.utils.config import config

from pypnnomenclature.models import TNomenclatures

from .fixtures import *
from .utils import set_logged_user

import sqlalchemy as sa

gn_module_validation = pytest.importorskip("gn_module_validation")
pytestmark = pytest.mark.skipif(
"VALIDATION" in config["DISABLED_MODULES"], reason="Validation is disabled"
)


@pytest.fixture()
def validation_with_max_score_and_wait_validation_status():
id_nomenclature_attente_validation = db.session.scalar(
sa.select(TNomenclatures.id_nomenclature).filter_by(mnemonique="En attente de validation")
)

validations_to_update = db.session.scalars(
sa.select(
TValidations.id_validation,
VLatestValidations.uuid_attached_row,
VConsistancyData.id_synthese,
)
.join(TValidations, TValidations.id_validation == VLatestValidations.id_validation)
.join(VConsistancyData, VConsistancyData.id_sinp == VLatestValidations.uuid_attached_row)
.where(
TValidations.validation_auto == True,
VLatestValidations.id_nomenclature_valid_status == id_nomenclature_attente_validation,
VLatestValidations.id_validator == None,
VConsistancyData.valid_phenology == True,
VConsistancyData.valid_altitude == True,
VConsistancyData.valid_distribution == True,
)
).all()
return validations_to_update


@pytest.mark.usefixtures("client_class", "temporary_transaction", "app")
class TestValidation:
def test_get_synthese_data(self, users, synthese_data):
Expand Down Expand Up @@ -103,3 +132,48 @@ def test_get_validation_history(self, users, synthese_data):
assert response.status_code == 200
assert len(response.data) > 0
assert response.json[0]["id_status"] == str(id_nomenclature_valid_status.id_nomenclature)

def test_auto_validation(
self,
users,
app,
auto_validation_enabled,
validation_with_max_score_and_wait_validation_status,
):
# fct_auto_validation_name = app.config["VALIDATION"]["AUTO_VALIDATION_SQL_FUNCTION"]
set_logged_user(self.client, users["user"])

id_nomenclature_probable = db.session.scalar(
sa.select(TNomenclatures.id_nomenclature).filter_by(mnemonique="Probable")
)
id_nomenclature_attente_validation = db.session.scalar(
sa.select(TNomenclatures.id_nomenclature).filter_by(
mnemonique="En attente de validation"
)
)

list_synthese_to_update = []
for row in validation_with_max_score_and_wait_validation_status:
list_synthese_to_update.append(row[2])

jacquesfize marked this conversation as resolved.
Show resolved Hide resolved
synthese_valid_statut_before_update = db.session.scalars(
sa.select(Synthese.id_nomenclature_valid_status).where(
Synthese.id_synthese.in_(list_synthese_to_update)
)
).all()
assert all(
synthese_valid_statut[0] == id_nomenclature_attente_validation
for synthese_valid_statut in synthese_valid_statut_before_update
)

# On applique la fonction
set_auto_validation() # list_synthese_updated = TValidations.auto_validation(fct_auto_validation_name)
synthese_valid_statut_after_update = db.session.scalars(
sa.select(Synthese.id_nomenclature_valid_status).where(
Synthese.id_synthese.in_(list_synthese_to_update)
)
).all()
assert all(
synthese_valid_statut[0] == id_nomenclature_probable
for synthese_valid_statut in synthese_valid_statut_after_update
)
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
from werkzeug.exceptions import BadRequest
from geonature.core.gn_commons.models import TValidations
from geonature.core.notifications.utils import dispatch_notifications

import gn_module_validation.tasks

blueprint = Blueprint("validation", __name__)
log = logging.getLogger()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,3 +83,6 @@ class GnModuleSchemaConf(Schema):
MAIL_BODY = fields.String(load_default=MAIL_BODY)
MAIL_SUBJECT = fields.String(load_default=MAIL_SUBJECT)
COLUMN_LIST = fields.List(fields.Nested(ColumnSchema), load_default=COLUMN_LIST)
AUTO_VALIDATION_CRONTAB = fields.String(load_default="* 1 * * *")
AUTO_VALIDATION_ENABLED = fields.Boolean(load_default=False)
AUTO_VALIDATION_SQL_FUNCTION = fields.String(load_default="fct_auto_validation")
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
"""add_fct_auto_validation

Revision ID: 9a4b4b6f8fe6
Revises: 446e902a14e7
Create Date: 2023-10-25 17:18:04.438706

"""
from alembic import op
import sqlalchemy as sa


# revision identifiers, used by Alembic.
revision = "9a4b4b6f8fe6"
down_revision = "df93a68242ee"
jacquesfize marked this conversation as resolved.
Show resolved Hide resolved
branch_labels = None
depends_on = ("f06cc80cc8ba",) # gn_commons

schema = "gn_profiles"
fct_name = "fct_auto_validation"
jacquesfize marked this conversation as resolved.
Show resolved Hide resolved


def upgrade():
op.execute(
sa.text(
f"""
create or replace function gn_profiles.fct_auto_validation (
new_validation_status int default 2,
score int default 3
) returns int language plpgsql as $function$
declare old_validation_status int := 0;

validation_id_type int := ref_nomenclatures.get_id_nomenclature_type('STATUT_VALID');

-- Retrieve the new validation status's nomenclature id
new_id_status_validation int := (
select tn.id_nomenclature
from ref_nomenclatures.t_nomenclatures tn
where tn.cd_nomenclature = new_validation_status::varchar
and id_type = validation_id_type
);

-- Retrieve the old validation status nomenclature id
old_id_status_validation int := (
select tn.id_nomenclature
from ref_nomenclatures.t_nomenclatures tn
where tn.cd_nomenclature = old_validation_status::varchar
and id_type = validation_id_type
);

-- Retrieve the list of observations tagged with the old validation status
list_uuid_obs_status_updatable uuid [] := (
select array_agg(vlv.uuid_attached_row)
from gn_commons.v_latest_validation vlv
join gn_profiles.v_consistancy_data vcd on vlv.uuid_attached_row = vcd.id_sinp
and (
(
vcd.valid_phenology::int + vcd.valid_altitude::int + vcd.valid_distribution::int
) = score
)
where vlv.id_nomenclature_valid_status = old_id_status_validation
and id_validator is null
);

number_of_obs_to_update int := array_length(list_uuid_obs_status_updatable, 1);
begin if number_of_obs_to_update > 0 then
raise notice '% observations seront validées automatiquement',number_of_obs_to_update;
-- Update Validation status
insert into gn_commons.t_validations (uuid_attached_row, id_nomenclature_valid_status, validation_auto, id_validator, validation_comment, validation_date)
select t_uuid.uuid_attached_row, new_id_status_validation ,true, null,'auto = default value',CURRENT_TIMESTAMP
from
(select distinct on (uuid_attached_row) uuid_attached_row
from gn_commons.t_validations tv
where uuid_attached_row = any (list_uuid_obs_status_updatable)
) t_uuid;
else
raise notice 'Aucune entrée dans les dernières observations n''est candidate à la validation automatique';
end if;
return 0;
end;
$function$;
"""
)
)


def downgrade():
op.execute(
sa.text(
f"""
DROP FUNCTION {schema}.{fct_name}
"""
)
)
37 changes: 37 additions & 0 deletions contrib/gn_module_validation/backend/gn_module_validation/tasks.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
from celery.schedules import crontab
from celery.utils.log import get_task_logger

from geonature.core.gn_commons.models import TValidations
from geonature.utils.celery import celery_app
from geonature.utils.config import config

logger = get_task_logger(__name__)


@celery_app.on_after_finalize.connect
def setup_periodic_tasks(sender, **kwargs):
is_enabled = config["VALIDATION"]["AUTO_VALIDATION_ENABLED"]
ct = config["VALIDATION"]["AUTO_VALIDATION_CRONTAB"]
if ct and is_enabled:
minute, hour, day_of_month, month_of_year, day_of_week = ct.split(" ")
sender.add_periodic_task(

Check warning on line 17 in contrib/gn_module_validation/backend/gn_module_validation/tasks.py

View check run for this annotation

Codecov / codecov/patch

contrib/gn_module_validation/backend/gn_module_validation/tasks.py#L13-L17

Added lines #L13 - L17 were not covered by tests
crontab(
minute=minute,
hour=hour,
day_of_week=day_of_week,
day_of_month=day_of_month,
month_of_year=month_of_year,
),
set_auto_validation.s(),
name="auto validation",
)


@celery_app.task(bind=True)
def set_auto_validation(self):
jacquesfize marked this conversation as resolved.
Show resolved Hide resolved
is_enabled = config["VALIDATION"]["AUTO_VALIDATION_ENABLED"]
fct_auto_validation_name = config["VALIDATION"]["AUTO_VALIDATION_SQL_FUNCTION"]
if is_enabled:
logger.info("Set autovalidation...")
TValidations.auto_validation(fct_auto_validation_name)
logger.info("Auto validation done")
jacquesfize marked this conversation as resolved.
Show resolved Hide resolved
jacquesfize marked this conversation as resolved.
Show resolved Hide resolved
1 change: 1 addition & 0 deletions contrib/gn_module_validation/setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
"blueprint = gn_module_validation.blueprint:blueprint",
"config_schema = gn_module_validation.conf_schema_toml:GnModuleSchemaConf",
"migrations = gn_module_validation:migrations",
"tasks = gn_module_validation.tasks",
],
},
classifiers=[
Expand Down
16 changes: 16 additions & 0 deletions contrib/gn_module_validation/validation_config.toml.example
Original file line number Diff line number Diff line change
@@ -1,3 +1,17 @@
# PARAMETRES CONFIG MODULE VALIDATION

# Uncomment the following line to enable the auto-validation (value must be equal to true)
#AUTO_VALIDATION_ENABLED = true

### Cron parameters for the auto-validation
#AUTO_VALIDATION_CRONTAB = "* 1 * * *"

## If the auto-validation function does not fit your usecase, be sure to create your own
## and indicate its name in the following variable
## ATTENTION : Be sure that your function is declared in the `gn_profiles` schema in the database

#AUTO_VALIDATION_SQL_FUNCTION = "fct_auto_validation_custom"

[[COLUMN_LIST]]
column_label = "Taxon"
column_name = "taxref.nom_vern_or_lb_nom"
Expand All @@ -23,3 +37,5 @@ min_width = 100
#column_label = "Stade de vie"
#column_name = "nomenclature_life_stage.label_default"
#min_width = 50


40 changes: 40 additions & 0 deletions docs/admin-manual.rst
Original file line number Diff line number Diff line change
Expand Up @@ -529,6 +529,7 @@ Ces profils sont déclinés sur :
- Le module de validation permet d'attirer l'attention des validateurs sur les données qui sortent du "cadre" déjà connu pour le taxon considéré, et d'apporter des éléments de contexte en complément de la donnée en cours de validation
- Le module Synthèse (fiche d'information, onglet validation) permet d'apporter des éléments de contexte en complément des données brutes consultées
- Le module Occtax permet d'alerter les utilisateurs lors de la saisie de données qui sortent du "cadre" déjà connu pour un taxon considéré
- Le processus de validation automatique permet de valider automatiquement les observations respectant le profil de taxons (non activé par défaut).

.. image :: https://raw.githubusercontent.com/PnX-SI/GeoNature/develop/docs/images/validation.png
.. image :: https://raw.githubusercontent.com/PnX-SI/GeoNature/develop/docs/images/contexte_donnee.png
Expand Down Expand Up @@ -2371,3 +2372,42 @@ Liste des propriétés disponibles :
- data_link : lien vers l'observation dans son module de saisie
- tous les champs de la synthèse (acquisition_framework, altitude_max, altitude_min, bio_status, blurring, cd_hab, cd_nom, comment_context, comment_description, date_min, depth_max, depth_min, determiner, diffusion_level, digital_proof, entity_source_pk_value, exist_proof, grp_method, grp_typ, last_action, life_stage, meta_create_date, meta_update_date, meta_v_taxref, meta_validation_date, nat_obj_geo, naturalness, nom_cite, non_digital_proof, obj_count, obs_technique, observation_status, observers, occ_behaviour, occ_stat_biogeo, place_name, precision, sample_number_proof, sensitivity, sex, source, type_count, unique_id_sinp, unique_id_sinp_grp, valid_status, validation_comment)
- tous les champs du taxon (cd_nom, cd_ref, cd_sup, cd_taxsup, regne, ordre, classe, famille, group1_inpn, group2_inpn, id_rang, nom_complet, nom_habitat, nom_rang, nom_statut, nom_valide, nom_vern)

Validation automatique
""""""""""""""""""""""
Depuis la version 2.14, il est possible d'activer la validation automatique d'observations.

Activation
``````````
L'activation de la validation automatique s'effectue en ajoutant la ligne suivante dans le fichier de configuration du module de validation ``config/validation_config.toml``:

::

AUTO_VALIDATION_ENABLED = true

Condition de validation automatique
```````````````````````````````````
Une observation sera validée automatiquement si elle rencontre les conditions suivantes:
* Son statut de validation est ``En attente de validation``
* Si le score calculé à partir du profil de taxons est de 3. Se référer à la section `Profils de taxons`_ pour plus d'informations.

Si ces conditions sont remplies, alors le statut de validation de l'observation est mis à ``Probable``.

**Notes** Si le comportement de validation automatique ne vous correspond pas, il est possible de définir soi-même ce dernier dans la base de données sous forme d'une fonction. Reportez-vous à la section `Modification de la fonction de validation automatique`_ pour plus d'informations.

Modification de la périodicité de la validation automatique
```````````````````````````````````````````````````````````

Le processus de validation automatique est executé à une fréquence définie, par défaut toutes les heures. Si toutefois, vous souhaitez diminuer ou augmenter la durée entre chaque validation automatique, définissez cette dernière dans le fichier de configuration (``config/validation_config.toml``) dans la variable ``AUTO_VALIDATION_CRONTAB``.

::

AUTO_VALIDATION_CRONTAB ="*/1 * * * *"

Ce paramètre est composé de cinq valeurs, chacune séparée par un espace: minute, heure, jour du mois, mois de l'année, journée de la semaine. Dans l'exemple ci-dessus, il est indiqué que le processus d'auto-validation sera répété toutes les minutes. Pour plus d'informations, vous pouvez consulter la documentation de Celery à ce sujet : https://docs.celeryq.dev/en/stable/userguide/periodic-tasks.html#crontab-schedules.

**Note** Si vous ne voulez pas définir un des paramètres de périodicité, utilisez un astérisque (``*``).

Modification de la fonction de validation automatique
`````````````````````````````````````````````````````
Dans GeoNature, la validation automatique est effectuée par une fonction en ``PL/pgSQL`` déclarée dans le schéma ``gn_profiles``. Si toutefois, le fonctionnement de celle-ci ne correspond à vos besoins, indiquer le nom de la nouvelle fonction dans la variable ``AUTO_VALIDATION_SQL_FUNCTION``. Attention, cette fonction doit aussi être stockée dans le schema ``gn_profiles``. Pour vous aidez, n'hésitez pas à regarder la définition de la fonction par défaut nommée ``fct_auto_validation``.