Skip to content

Commit

Permalink
Merge branch 'feat/individuals-widget' into feat/individuals
Browse files Browse the repository at this point in the history
  • Loading branch information
Maxime Vergez committed Nov 10, 2023
2 parents 824be7c + 0881517 commit c542314
Show file tree
Hide file tree
Showing 14 changed files with 364 additions and 20 deletions.
27 changes: 12 additions & 15 deletions backend/geonature/core/gn_monitoring/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ class TBaseSites(DB.Model):
DB.Column(
"id_individual",
DB.Integer,
DB.ForeignKey("gn_monitoring.t_individual_complements.id_individual", ondelete="CASCADE"),
DB.ForeignKey("gn_monitoring.t_individuals.id_individual", ondelete="CASCADE"),
primary_key=True,
),
DB.Column(
Expand Down Expand Up @@ -215,16 +215,22 @@ class TIndividuals(DB.Model):
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],
)


@serializable
class TMarkingEvent(TIndividuals):
class TMarkingEvent(DB.Model):
__tablename__ = "t_marking_events"
__table_args__ = {"schema": "gn_monitoring"}
__mapper_args__ = {
"polymorphic_identity": "monitoring_marking_event",
}

id_marking = DB.Column(DB.Integer, primary_key=True)
id_marking = DB.Column(DB.Integer, primary_key=True, autoincrement=True)
id_individual = DB.Column(
DB.ForeignKey(f"gn_monitoring.t_individuals.id_individual", ondelete="CASCADE"),
nullable=False,
Expand All @@ -246,13 +252,4 @@ class TMarkingEvent(TIndividuals):
marking_details = DB.Column(DB.Text)
data = DB.Column(JSONB)

modules = DB.relationship(
"TModules",
lazy="select",
enable_typechecks=False,
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],
)
# meta_update_date and meta_create_date already present in TIndividuals
25 changes: 22 additions & 3 deletions backend/geonature/core/gn_monitoring/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@
from geonature.core.gn_monitoring.schema import TIndividualsSchema
from marshmallow import ValidationError, EXCLUDE
from sqlalchemy.sql import func
from sqlalchemy.orm import raiseload, joinedload
from geojson import FeatureCollection
from werkzeug.exceptions import BadRequest

from geonature.core.gn_commons.models import TModules
from geonature.core.gn_permissions.decorators import login_required
from geonature.utils.env import DB
from geonature.core.gn_monitoring.models import (
Expand Down Expand Up @@ -114,9 +116,18 @@ def get_site_areas(id_site):
@routes.route("/individuals", methods=["GET"])
@login_required
def get_individuals():
params = request.args
id_module = params.get("id_module")
query = TIndividuals.query

schema = TIndividualsSchema()
if id_module:
query = query.options(
raiseload("*"),
joinedload(TIndividuals.modules),
joinedload(TIndividuals.nomenclature_sex),
joinedload(TIndividuals.digitiser),
).filter(TIndividuals.modules.any(TModules.id_module == id_module))

schema = TIndividualsSchema(exclude=["modules"])
# In the future: paginate the query. But need infinite scroll on
# select frontend side
return schema.jsonify(query.all(), many=True)
Expand All @@ -125,14 +136,22 @@ def get_individuals():
@routes.route("/individual", methods=["POST"])
@login_required
def create_one_individual():
# Id module is an optional parameter to associate an individual
# to a module
id_module = request.args.get("id_module")
module = None
if id_module is not None:
module = TModules.query.get_or_404(id_module)

# 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 152 in backend/geonature/core/gn_monitoring/routes.py

View check run for this annotation

Codecov / codecov/patch

backend/geonature/core/gn_monitoring/routes.py#L151-L152

Added lines #L151 - L152 were not covered by tests

if module is not None:
individual.modules = [module]
DB.session.add(individual)
DB.session.commit()
return individual_schema.jsonify(individual)
5 changes: 4 additions & 1 deletion backend/geonature/core/gn_monitoring/schema.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
from geonature.utils.env import MA
from marshmallow import fields

from geonature.core.gn_commons.schemas import ModuleSchema
from geonature.utils.env import MA
from geonature.core.gn_monitoring.models import TIndividuals
from pypnnomenclature.schemas import NomenclatureSchema
from pypnusershub.schemas import UserSchema
Expand All @@ -13,3 +15,4 @@ class Meta:

nomenclature_sex = MA.Nested(NomenclatureSchema, dump_only=True)
digitiser = MA.Nested(UserSchema, dump_only=True)
modules = fields.List(MA.Nested(ModuleSchema, dump_only=True))
39 changes: 39 additions & 0 deletions backend/geonature/tests/test_monitoring.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
from geonature.utils.env import db
from pypnusershub.tests.utils import set_logged_user_cookie

from .fixtures import *

CD_NOM = 212


Expand Down Expand Up @@ -38,6 +40,25 @@ def test_get_individuals(self, users, individuals):
}

assert expected_individuals_uuid.issubset(individuals_uuid_from_response)
assert all("module" not in individual for individual in json_resp)

def test_get_individuals_with_id_module(self, users, individuals, module):
set_logged_user_cookie(self.client, users["self_user"])

# Add individual to module X
with db.session.begin_nested():
individuals[0].modules = [module]

response = self.client.get(
url_for("gn_monitoring.get_individuals"), query_string={"id_module": module.id_module}
)
resp_json = response.json
not_expected_individual_uuid = {individuals[1].uuid_individual}
expected_individual_uuid = {individuals[0].uuid_individual}
actual_individual_uuid = {individual["uuid_individual"] for individual in resp_json}

assert actual_individual_uuid.isdisjoint(not_expected_individual_uuid)
assert actual_individual_uuid.issubset(expected_individual_uuid)

def test_create_one_individual(self, users):
set_logged_user_cookie(self.client, users["self_user"])
Expand All @@ -51,3 +72,21 @@ def test_create_one_individual(self, users):
json_resp = response.json
assert json_resp["cd_nom"] == CD_NOM
assert json_resp["individual_name"] == individual_name

def test_create_one_individual_id_module(self, users, module):
set_logged_user_cookie(self.client, users["self_user"])
individual_name = "Test_Post"
individual = {"individual_name": individual_name, "cd_nom": CD_NOM}

response = self.client.post(
url_for("gn_monitoring.create_one_individual"),
query_string={"id_module": module.id_module},
json=individual,
)

json_resp = response.json
assert json_resp["cd_nom"] == CD_NOM
assert json_resp["individual_name"] == individual_name
modules = json_resp["modules"]
assert len(modules) == 1
assert modules[0]["id_module"] == module.id_module
7 changes: 7 additions & 0 deletions frontend/src/app/GN2CommonModule/GN2Common.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,9 @@ import { MediaService } from '@geonature_common/service/media.service';
import { NgbDatePeriodParserFormatter } from '@geonature_common/form/date/ngb-date-custom-parser-formatter';
import { SyntheseDataService } from '@geonature_common/form/synthese-form/synthese-data.service';
import { TaxonTreeComponent } from './form/taxon-tree/taxon-tree.component';
import { IndividualsComponent } from './form/individuals/individuals.component';
import { IndividualsService } from './form/individuals/individuals.service';
import { IndividualsCreateComponent } from './form/individuals/create/individuals-create.component';

@NgModule({
imports: [
Expand Down Expand Up @@ -192,6 +195,8 @@ import { TaxonTreeComponent } from './form/taxon-tree/taxon-tree.component';
TaxonAdvancedModalComponent,
TaxonomyComponent,
TaxonTreeComponent,
IndividualsComponent,
IndividualsCreateComponent,
],
providers: [
CommonService,
Expand All @@ -205,6 +210,7 @@ import { TaxonTreeComponent } from './form/taxon-tree/taxon-tree.component';
NgbDatePeriodParserFormatter,
SyntheseDataService,
TranslateService,
IndividualsService,
],
exports: [
AcquisitionFrameworksComponent,
Expand Down Expand Up @@ -296,6 +302,7 @@ import { TaxonTreeComponent } from './form/taxon-tree/taxon-tree.component';
TaxonomyComponent,
TaxonTreeComponent,
TranslateModule,
IndividualsComponent,
],
})
export class GN2CommonModule {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -345,7 +345,14 @@
[filters]="formDefComp['filters']"
[default]="formDefComp['default']"
></pnx-datalist>

<pnx-individuals
*ngSwitchCase="'individuals'"
[parentFormControl]="form.get(formDefComp['attribut_name'])"
[label]="formDefComp['attribut_label']"
[idModule]="formDefComp['id_module']"
[idList]="formDefComp['id_list']"
[cdNom]="formDefComp['cd_nom']"
></pnx-individuals>
<small class="form-text text-muted">{{ formDefComp['help'] }}</small>
</div>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
<div class="modal-header">
<h5 class="modal-title">Création d'un individu</h5>
<button type="button" class="close" aria-label="Close" (click)="cancelCreate()">
<span aria-hidden="true">&times;</span>
</button>
</div>

<form class="modal-content">
<div class="modal-body" id="bodyModal" style="padding-top: 30px">
<small>Nom de l'individu</small>
<input
class="form-control form-control-sm"
type="text"
data-qa="pnx-individuals-create-individual-name"
[formControl]="form.get('individual_name')"
/>
<pnx-nomenclature
label="Sexe"
[parentFormControl]="form.get('id_nomenclature_sex')"
codeNomenclatureType="SEXE"
keyValue="id_nomenclature"
>
</pnx-nomenclature>
<small>Commentaires</small>
<textarea
class="form-control form-control-sm"
[formControl]="form.get('comment')"
type="textarea"
>
</textarea>
<pnx-taxonomy
*ngIf="cdNom === null"
label="Taxon"
[parentFormControl]="form.get('cd_nom_temp')"
[idList]="idList"
displayedLabel="nom_valide"
charNumber="3"
listLength="20"
(onChange)="taxonSelected($event)"
>
</pnx-taxonomy>
</div>
</form>
<div class="modal-footer">
<div class="d-flex justify-content-between">
<button id="cancelButton" type="button" color="warn" mat-raised-button (click)="cancelCreate()">
Annuler
</button>
<button
id="createButton"
type="button"
class="button-success"
mat-raised-button
(click)="createIndividual()"
>
Créer un individu
</button>
</div>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#createButton {
float: right;
//background-color: #1976D2;
border: none;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { Component, Output, EventEmitter, Input, OnInit } from '@angular/core';
import { FormControl, FormGroup } from '@angular/forms';
import { Validators } from '@angular/forms';
import { Taxon } from '@geonature_common/form/taxonomy/taxonomy.component';
import { NgbTypeaheadSelectItemEvent } from '@ng-bootstrap/ng-bootstrap';
import { IndividualsService } from '../individuals.service';
import { Individual } from '../interfaces';
import { throwError } from 'rxjs';
@Component({
selector: 'pnx-individuals-create',
templateUrl: './individuals-create.component.html',
styleUrls: ['./individuals-create.component.scss'],
})
export class IndividualsCreateComponent implements OnInit {
@Input() idModule: null | number = null;
@Input() idList: null | string = null;
@Input() cdNom: null | number = null;
@Output() individualEvent = new EventEmitter<Individual>();
@Output() cancelEvent = new EventEmitter();

form: FormGroup<{
individual_name: FormControl<string>;
id_nomenclature_sex: FormControl<number | null>;
cd_nom: FormControl<number | null>;
cd_nom_temp: FormControl<number | null>;
comment: FormControl<string>;
}>;

constructor(private _individualsService: IndividualsService) {}

ngOnInit() {
this.form = new FormGroup({
individual_name: new FormControl<string>('', {
validators: [Validators.required],
}),
id_nomenclature_sex: new FormControl<number | null>(null),
cd_nom: new FormControl<number | null>(this.cdNom, {
validators: [Validators.required],
}),
// Normally we could avoid providing a form to taxonomy
// widget, but it needs it and affecting the entire taxon
// to the form control. Creating a temp one to fix this problem
cd_nom_temp: new FormControl<number | null>(this.cdNom, {
validators: [Validators.required],
}),
comment: new FormControl<string>(''),
});
}

taxonSelected(value: NgbTypeaheadSelectItemEvent<Taxon>) {
this.form.patchValue({ cd_nom: value.item.cd_nom });
}

createIndividual() {
const value = this.form.getRawValue();
delete value.cd_nom_temp;
this._individualsService
.postIndividual(value as Individual, this.idModule)
.subscribe((value) => this.individualEvent.emit(value));
}

cancelCreate() {
this.cancelEvent.emit();
}
}
Loading

0 comments on commit c542314

Please sign in to comment.