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/individuals #3299

Open
wants to merge 27 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
4a15173
[Monitoring individuals] feat(db): migration for individuals and new …
Oct 4, 2023
5fc32fc
[Monitoring individuals] feat(api): Add route for individuals : orm m…
Oct 4, 2023
6834b02
[Monitoring individuals] feat(front): Create: individual widget
Oct 31, 2023
9cc732a
[Monitoring individuals] fix(api): mistakes in query
Oct 31, 2023
1744bc5
[Monitoring individuals] feat(api): now possible to specify module
Oct 31, 2023
af6d2a9
[Monitoring individuals] feat(front): add create component for indivi…
Oct 31, 2023
6bcf866
[Monitoring individuals] feat(api): remove 1 to 1 relationship
Nov 2, 2023
12333ab
[Monitoring individuals] fix(db): add default date for meta and update
Nov 10, 2023
33b4649
[Monitoring individuals] fix(api): remove raise load
Nov 10, 2023
a9adb10
[Monitoring individuals] fix(front): add undefined condition
Nov 22, 2023
aee2fcb
[Monitoring individuals] feat(api): Crud in routes + lint
Nov 22, 2023
92ad959
[Monitoring individuals] feat(front): make idModule compulsory
Dec 19, 2023
62ae32b
[Monitoring individuals] test(api): add permissions testing & fixtures
Dec 20, 2023
fd0a780
[Monitoring individuals] feat(db): add id_digitiser for marking for crud
Dec 21, 2023
62571af
[Monitoring individuals] feat(front): set name default null & disable…
Dec 21, 2023
4d97336
[Monitoring individuals] feat(api): add marking schema & relationship
Dec 21, 2023
e328a89
[Monitoring individuals] fix(db): update revision
Jan 8, 2024
f306604
[Monitoring individuals] Rebase develop
amandine-sahl Dec 31, 2024
4647600
[Monitoring individuals] (feat) monitoring individuals : implementati…
amandine-sahl Dec 31, 2024
977c18b
[Monitoring individuals] fix : prettier
amandine-sahl Dec 31, 2024
2393263
[Monitoring individuals] [db] monitoring : add id_individuals to t_ob…
amandine-sahl Jan 2, 2025
006c44f
[Monitoring individuals] feat: add medias to tmarkings_events and ind…
andriacap Jan 6, 2025
5c75843
[Monitoring individuals] fix filter active individuals only in indivi…
andriacap Jan 6, 2025
adff0b6
[Monitoring individuals] chore: Ajout de *.DS_Store au .gitignore
andriacap Jan 6, 2025
681c799
[Monitoring individuals] [feat] Indiduals medias : add schema
amandine-sahl Jan 7, 2025
d13fe4f
[Monitoring] Add t_observations.cd_nom foreign key
amandine-sahl Jan 7, 2025
81d9215
[Monitoring individuals] Add trigger calculate t_observations.cd_nom …
amandine-sahl Jan 7, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -154,5 +154,7 @@ install_all/install_all.log

/docs/CHANGELOG.html


/contrib/*/frontend/node_modules
Makefile.local
Makefile.local
*.DS_Store
203 changes: 199 additions & 4 deletions backend/geonature/core/gn_monitoring/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,25 @@
relatifs aux protocoles de suivis
"""

from flask import g
from datetime import datetime

from geoalchemy2 import Geometry
from sqlalchemy import ForeignKey
from sqlalchemy import ForeignKey, or_, false
from sqlalchemy.orm import relationship
from sqlalchemy.dialects.postgresql import UUID, JSONB
from sqlalchemy.sql import select, func
from sqlalchemy.schema import FetchedValue
from sqlalchemy.ext.hybrid import hybrid_property


from pypnnomenclature.models import TNomenclatures
from pypnusershub.db.models import User
from ref_geo.models import LAreas
from utils_flask_sqla.serializers import serializable
from utils_flask_sqla_geo.serializers import geoserializable

from pypnnomenclature.models import TNomenclatures
from geonature.core.gn_commons.models import TModules
from geonature.core.gn_commons.models import TModules, TMedias
from geonature.core.gn_meta.models import TDatasets
from geonature.utils.env import DB

Expand Down Expand Up @@ -213,6 +218,24 @@
)


corIndividualModule = DB.Table(
"cor_individual_module",
DB.Column(
"id_individual",
DB.Integer,
DB.ForeignKey("gn_monitoring.t_individuals.id_individual", ondelete="CASCADE"),
primary_key=True,
),
DB.Column(
"id_module",
DB.Integer,
DB.ForeignKey("gn_commons.t_modules.id_module", ondelete="CASCADE"),
primary_key=True,
),
schema="gn_monitoring",
)


@serializable
class TObservations(DB.Model):
__tablename__ = "t_observations"
Expand All @@ -223,6 +246,178 @@
digitiser = DB.relationship(
User, primaryjoin=(User.id_role == id_digitiser), foreign_keys=[id_digitiser]
)
cd_nom = DB.Column(DB.Integer)
cd_nom = DB.Column(DB.Integer, DB.ForeignKey("taxonomie.taxref.cd_nom"), nullable=False)
comments = DB.Column(DB.String)
uuid_observation = DB.Column(UUID(as_uuid=True), default=select(func.uuid_generate_v4()))

id_individual = DB.Column(DB.ForeignKey("gn_monitoring.t_individuals.id_individual"))


@serializable
class TMarkingEvent(DB.Model):
__tablename__ = "t_marking_events"
__table_args__ = {"schema": "gn_monitoring"}

id_marking = DB.Column(DB.Integer, primary_key=True, autoincrement=True)
uuid_marking = DB.Column(UUID(as_uuid=True), default=select(func.uuid_generate_v4()))
id_individual = DB.Column(
DB.ForeignKey(f"gn_monitoring.t_individuals.id_individual", ondelete="CASCADE"),
nullable=False,
)
id_module = DB.Column(
DB.ForeignKey("gn_commons.t_modules.id_module"),
primary_key=True,
nullable=False,
unique=True,
)
id_digitiser = DB.Column(
DB.ForeignKey("utilisateurs.t_roles.id_role"),
nullable=False,
)
marking_date = DB.Column(DB.DateTime(timezone=False), nullable=False)
id_operator = DB.Column(DB.ForeignKey("utilisateurs.t_roles.id_role"), nullable=False)
id_base_marking_site = DB.Column(DB.ForeignKey("gn_monitoring.t_base_sites.id_base_site"))
id_nomenclature_marking_type = DB.Column(
DB.ForeignKey("ref_nomenclatures.t_nomenclatures.id_nomenclature"), nullable=False
)
marking_location = DB.Column(DB.Unicode(255))
marking_code = DB.Column(DB.Unicode(255))
marking_details = DB.Column(DB.Text)
data = DB.Column(JSONB)

operator = DB.relationship(User, lazy="joined", foreign_keys=[id_operator])

digitiser = DB.relationship(User, lazy="joined", foreign_keys=[id_digitiser])

medias = DB.relationship(
TMedias,
lazy="joined",
primaryjoin=(TMedias.uuid_attached_row == uuid_marking),
foreign_keys=[TMedias.uuid_attached_row],
overlaps="medias,medias",
)

@hybrid_property
def organism_actors(self):
# return self.digitiser.id_organisme
actors_organism_list = []
if isinstance(self.digitiser, User):
actors_organism_list.append(self.digitiser.id_organisme)
if isinstance(self.operator, User):
actors_organism_list.append(self.operator.id_organisme)
return actors_organism_list

def has_instance_permission(self, scope):
if scope == 0:
return False
elif scope in (1, 2):
if (
g.current_user.id_role == self.id_digitiser
or g.current_user.id_role == self.id_operator
):
return True
if scope == 2 and g.current_user.id_organisme in self.organism_actors:
return True
elif scope == 3:
return True
return False


@serializable
class TIndividuals(DB.Model):
__tablename__ = "t_individuals"
__table_args__ = {"schema": "gn_monitoring"}
id_individual = DB.Column(DB.Integer, primary_key=True)
uuid_individual = DB.Column(UUID, nullable=False, server_default=DB.text("uuid_generate_v4()"))
individual_name = DB.Column(DB.Unicode(255), nullable=False)
cd_nom = DB.Column(DB.Integer, DB.ForeignKey("taxonomie.taxref.cd_nom"), nullable=False)
id_nomenclature_sex = DB.Column(
DB.ForeignKey("ref_nomenclatures.t_nomenclatures.id_nomenclature"),
server_default=DB.text(
"ref_nomenclatures.get_default_nomenclature_value('SEXE'::character varying)"
),
)
active = DB.Column(DB.Boolean, default=True)
comment = DB.Column(DB.Text)
id_digitiser = DB.Column(
DB.ForeignKey("utilisateurs.t_roles.id_role"),
nullable=False,
)

meta_create_date = DB.Column(
"meta_create_date", DB.DateTime(timezone=False), server_default=FetchedValue()
)
meta_update_date = DB.Column(
"meta_update_date",
DB.DateTime(timezone=False),
server_default=FetchedValue(),
onupdate=datetime.now,
)

digitiser = DB.relationship(
User,
lazy="joined",
)

nomenclature_sex = DB.relationship(
TNomenclatures,
lazy="select",
primaryjoin=(TNomenclatures.id_nomenclature == id_nomenclature_sex),
)

modules = DB.relationship(
"TModules",
lazy="joined",
secondary=corIndividualModule,
primaryjoin=(corIndividualModule.c.id_individual == id_individual),
secondaryjoin=(corIndividualModule.c.id_module == TModules.id_module),
foreign_keys=[corIndividualModule.c.id_individual, corIndividualModule.c.id_module],
)

markings = DB.relationship(
TMarkingEvent,
primaryjoin=(id_individual == TMarkingEvent.id_individual),
)

medias = DB.relationship(
TMedias,
lazy="joined",
primaryjoin=(TMedias.uuid_attached_row == uuid_individual),
foreign_keys=[TMedias.uuid_attached_row],
overlaps="medias",
)

@classmethod
def filter_by_scope(cls, query, scope, user):
if scope == 0:
query = query.where(false())

Check warning on line 393 in backend/geonature/core/gn_monitoring/models.py

View check run for this annotation

Codecov / codecov/patch

backend/geonature/core/gn_monitoring/models.py#L393

Added line #L393 was not covered by tests
elif scope in (1, 2):
ors = [
cls.id_digitiser == user.id_role,
]
# if organism is None => do not filter on id_organism even if level = 2
if scope == 2 and user.id_organisme is not None:
ors.append(cls.digitiser.has(id_organisme=user.id_organisme))
query = query.where(or_(*ors))
return query

@hybrid_property
def organism_actors(self):
# return self.digitiser.id_organisme
actors_organism_list = []
if isinstance(self.digitiser, User):
actors_organism_list.append(self.digitiser.id_organisme)

return actors_organism_list

def has_instance_permission(self, scope):
if scope == 0:
return False
elif scope in (1, 2):
if g.current_user.id_role == self.id_digitiser:
return True
if scope == 2 and g.current_user.id_organisme in self.organism_actors:
return True
elif scope == 3:
return True
return False
83 changes: 81 additions & 2 deletions backend/geonature/core/gn_monitoring/routes.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,24 @@
from flask import Blueprint, request
from flask import Blueprint, request, g
from geonature.core.gn_monitoring.schema import TIndividualsSchema
from geonature.core.gn_permissions.tools import get_scope
from marshmallow import ValidationError, EXCLUDE
from sqlalchemy.sql import func, select
from geojson import FeatureCollection
from geonature.core.gn_monitoring.models import TBaseSites, cor_site_area, cor_site_module
from werkzeug.exceptions import BadRequest, Forbidden, NotFound

from geonature.core.gn_commons.models import TModules
from geonature.core.gn_permissions.decorators import _forbidden_message, login_required
from geonature.utils.env import DB
from ref_geo.models import LAreas
from sqlalchemy import select
from sqlalchemy.sql import func
from geonature.core.gn_monitoring.models import (
TBaseSites,
TIndividuals,
cor_site_area,
cor_site_module,
)

from utils_flask_sqla.response import json_resp
from utils_flask_sqla_geo.generic import get_geojson_feature

Expand Down Expand Up @@ -98,3 +112,68 @@
feature["id"] = d[1]
features.append(feature)
return FeatureCollection(features)


@routes.route("/individuals/<int:id_module>", methods=["GET"])
@login_required
def get_individuals(id_module):
action = "R"
object_code = "MONITORINGS_INDIVIDUALS"
module = DB.session.get(TModules, id_module)
if module is None:
raise NotFound("Module not found")

Check warning on line 124 in backend/geonature/core/gn_monitoring/routes.py

View check run for this annotation

Codecov / codecov/patch

backend/geonature/core/gn_monitoring/routes.py#L124

Added line #L124 was not covered by tests
module_code = module.module_code
current_user = g.current_user
max_scope = get_scope(
action, id_role=current_user.id_role, module_code=module_code, object_code=object_code
)

if not max_scope:
raise Forbidden(description=_forbidden_message(action, module_code, object_code))

# FIXME: when all sqlalchemy 2.0 PR are merged, update it to fit the good practices
# like @qfilter etc...
query = select(TIndividuals).where(TIndividuals.modules.any(TModules.id_module == id_module))
results = (
DB.session.scalars(TIndividuals.filter_by_scope(query, max_scope, current_user))
.unique()
.all()
)

schema = TIndividualsSchema(exclude=["modules", "digitiser", "markings", "nomenclature_sex"])
# In the future: paginate the query. But need infinite scroll on
# select frontend side
return schema.jsonify(results, many=True)


@routes.route("/individual/<int:id_module>", methods=["POST"])
@login_required
def create_one_individual(id_module: int):
# Id module is an optional parameter to associate an individual
# to a module
action = "C"
object_code = "MONITORINGS_INDIVIDUALS"
module = DB.session.get(TModules, id_module)
if module is None:
raise NotFound("Module not found")

Check warning on line 158 in backend/geonature/core/gn_monitoring/routes.py

View check run for this annotation

Codecov / codecov/patch

backend/geonature/core/gn_monitoring/routes.py#L158

Added line #L158 was not covered by tests
module_code = module.module_code
current_user = g.current_user
max_scope = get_scope(
action, id_role=current_user.id_role, module_code=module_code, object_code=object_code
)

if not max_scope:
raise Forbidden(description=_forbidden_message(action, module_code, object_code))

# Exclude id_digitiser since it is set by the current user
individual_schema = TIndividualsSchema(exclude=["id_digitiser"], unknown=EXCLUDE)
individual_instance = TIndividuals(id_digitiser=g.current_user.id_role)
try:
individual = individual_schema.load(data=request.get_json(), instance=individual_instance)
except ValidationError as error:
raise BadRequest(error.messages)

Check warning on line 174 in backend/geonature/core/gn_monitoring/routes.py

View check run for this annotation

Codecov / codecov/patch

backend/geonature/core/gn_monitoring/routes.py#L173-L174

Added lines #L173 - L174 were not covered by tests

individual.modules = [module]
DB.session.add(individual)
DB.session.commit()
return individual_schema.jsonify(individual)
30 changes: 30 additions & 0 deletions backend/geonature/core/gn_monitoring/schema.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
from marshmallow import fields

from geonature.core.gn_commons.schemas import ModuleSchema, MediaSchema
from geonature.utils.env import MA
from geonature.core.gn_monitoring.models import TIndividuals, TMarkingEvent
from pypnnomenclature.schemas import NomenclatureSchema
from pypnusershub.schemas import UserSchema


class TMarkingEventSchema(MA.SQLAlchemyAutoSchema):
class Meta:
model = TMarkingEvent
include_fk = True
load_instance = True

operator = MA.Nested(UserSchema, dump_only=True)
medias = MA.Nested(MediaSchema, many=True)


class TIndividualsSchema(MA.SQLAlchemyAutoSchema):
class Meta:
model = TIndividuals
include_fk = True
load_instance = True

nomenclature_sex = MA.Nested(NomenclatureSchema, dump_only=True)
digitiser = MA.Nested(UserSchema, dump_only=True)
modules = fields.List(MA.Nested(ModuleSchema, dump_only=True))
markings = fields.List(MA.Nested(TMarkingEventSchema, dump_only=True))
medias = MA.Nested(MediaSchema, many=True)
Loading
Loading