From 46296b00d6457dceca5a65e553e8bc707f8b4a39 Mon Sep 17 00:00:00 2001 From: Limber Mamani Date: Fri, 21 Feb 2025 13:40:17 -0400 Subject: [PATCH 1/5] [TM-1665] chage collection tree-planted -> nursery-seedling to project report entity --- libs/database/src/lib/entities/project-report.entity.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/database/src/lib/entities/project-report.entity.ts b/libs/database/src/lib/entities/project-report.entity.ts index fd94a3f5..cdd83282 100644 --- a/libs/database/src/lib/entities/project-report.entity.ts +++ b/libs/database/src/lib/entities/project-report.entity.ts @@ -425,7 +425,7 @@ export class ProjectReport extends Model { @HasMany(() => TreeSpecies, { foreignKey: "speciesableId", constraints: false, - scope: { speciesableType: ProjectReport.LARAVEL_TYPE, collection: "tree-planted" } + scope: { speciesableType: ProjectReport.LARAVEL_TYPE, collection: "nursery-seedling" } }) treesPlanted: TreeSpecies[] | null; } From 01d8630150ee654c245d96a780ca3a2d1237b4b0 Mon Sep 17 00:00:00 2001 From: Limber Mamani Date: Fri, 21 Feb 2025 14:47:52 -0400 Subject: [PATCH 2/5] [TM-1665] change association --- libs/database/src/lib/entities/project-report.entity.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libs/database/src/lib/entities/project-report.entity.ts b/libs/database/src/lib/entities/project-report.entity.ts index cdd83282..86a6855c 100644 --- a/libs/database/src/lib/entities/project-report.entity.ts +++ b/libs/database/src/lib/entities/project-report.entity.ts @@ -32,7 +32,7 @@ type ApprovedIdsSubqueryOptions = { })) @Table({ tableName: "v2_project_reports", underscored: true, paranoid: true }) export class ProjectReport extends Model { - static readonly TREE_ASSOCIATIONS = ["treesPlanted"]; + static readonly TREE_ASSOCIATIONS = ["nurserySeedlings"]; static readonly PARENT_ID = "projectId"; static readonly APPROVED_STATUSES = ["approved"]; static readonly LARAVEL_TYPE = "App\\Models\\V2\\Projects\\ProjectReport"; @@ -427,5 +427,5 @@ export class ProjectReport extends Model { constraints: false, scope: { speciesableType: ProjectReport.LARAVEL_TYPE, collection: "nursery-seedling" } }) - treesPlanted: TreeSpecies[] | null; + nurserySeedlings: TreeSpecies[] | null; } From ef8c22f5a4285f3b2e3e594093ae2da6e9a8b542 Mon Sep 17 00:00:00 2001 From: Nathan Curtis Date: Thu, 27 Feb 2025 16:59:11 -0800 Subject: [PATCH 3/5] [TM-1805] Only return active polygons. --- .../src/site-polygons/site-polygon-query.builder.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/research-service/src/site-polygons/site-polygon-query.builder.ts b/apps/research-service/src/site-polygons/site-polygon-query.builder.ts index 9f30b608..2728a525 100644 --- a/apps/research-service/src/site-polygons/site-polygon-query.builder.ts +++ b/apps/research-service/src/site-polygons/site-polygon-query.builder.ts @@ -70,6 +70,8 @@ export class SitePolygonQueryBuilder extends PaginatedQueryBuilder { model: PolygonGeometry, attributes: ["polygon"], required: true }, this.siteJoin ]; + + this.where({ isActive: true }); } async excludeTestProjects() { From ac914ec19adead322c528556816600c16774a06e Mon Sep 17 00:00:00 2001 From: Nathan Curtis Date: Fri, 28 Feb 2025 14:37:25 -0800 Subject: [PATCH 4/5] [TM-1665] Fake out the FE by changing the collection on establishment species for PPC Project Reports --- .../src/trees/tree.service.spec.ts | 52 ++++++++++++------- apps/entity-service/src/trees/tree.service.ts | 18 +++++-- .../airtable/entities/airtable-entity.spec.ts | 6 +-- .../src/lib/entities/nursery.entity.ts | 5 ++ .../lib/factories/project-report.factory.ts | 2 + .../src/lib/factories/tree-species.factory.ts | 4 +- 6 files changed, 60 insertions(+), 27 deletions(-) diff --git a/apps/entity-service/src/trees/tree.service.spec.ts b/apps/entity-service/src/trees/tree.service.spec.ts index 0b3b128e..e9d6c240 100644 --- a/apps/entity-service/src/trees/tree.service.spec.ts +++ b/apps/entity-service/src/trees/tree.service.spec.ts @@ -68,23 +68,31 @@ describe("TreeService", () => { describe("getEstablishmentTrees", () => { it("should return establishment trees", async () => { - const project = await ProjectFactory.create(); - const projectReport = await ProjectReportFactory.create({ projectId: project.id }); - const site = await SiteFactory.create({ projectId: project.id }); + const tfProject = await ProjectFactory.create({ frameworkKey: "terrafund" }); + const tfProjectReport = await ProjectReportFactory.create({ projectId: tfProject.id, frameworkKey: "terrafund" }); + const site = await SiteFactory.create({ projectId: tfProject.id }); const siteReport = await SiteReportFactory.create({ siteId: site.id }); - const nursery = await NurseryFactory.create({ projectId: project.id }); + const nursery = await NurseryFactory.create({ projectId: tfProject.id }); const nurseryReport = await NurseryReportFactory.create({ nurseryId: nursery.id }); - const projectTreesPlanted = ( - await TreeSpeciesFactory.forProjectTreePlanted.createMany(3, { speciesableId: project.id }) + const ppcProject = await ProjectFactory.create({ frameworkKey: "ppc" }); + const ppcProjectReport = await ProjectReportFactory.create({ projectId: ppcProject.id, frameworkKey: "ppc" }); + + const tfProjectTrees = ( + await TreeSpeciesFactory.forProjectTreePlanted.createMany(3, { speciesableId: tfProject.id }) ) .map(({ name }) => name) .sort(); // hidden trees are ignored await TreeSpeciesFactory.forProjectTreePlanted.create({ - speciesableId: project.id, + speciesableId: tfProject.id, hidden: true }); + const ppcProjectTrees = ( + await TreeSpeciesFactory.forProjectTreePlanted.createMany(3, { speciesableId: ppcProject.id }) + ) + .map(({ name }) => name) + .sort(); const siteTreesPlanted = (await TreeSpeciesFactory.forSiteTreePlanted.createMany(2, { speciesableId: site.id })) .map(({ name }) => name) .sort(); @@ -106,25 +114,31 @@ describe("TreeService", () => { hidden: true }); - let result = await service.getEstablishmentTrees("project-reports", projectReport.uuid); + let result = await service.getEstablishmentTrees("project-reports", tfProjectReport.uuid); + expect(Object.keys(result).length).toBe(1); + expect(result["tree-planted"].sort()).toEqual(tfProjectTrees); + result = await service.getEstablishmentTrees("project-reports", ppcProjectReport.uuid); expect(Object.keys(result).length).toBe(1); - expect(result["tree-planted"].sort()).toEqual(projectTreesPlanted); + // for PPC Project Reports, we fake out the FE by changing the establishment collection from tree-planted to + // nursery seedling. This is to support the strange situation where project report trees are only ever + // nursery seedlings in PPC, but the establishment data is always tree-planted. + expect(result["nursery-seedling"].sort()).toEqual(ppcProjectTrees); result = await service.getEstablishmentTrees("sites", site.uuid); expect(Object.keys(result).length).toBe(1); - expect(result["tree-planted"].sort()).toEqual(projectTreesPlanted); + expect(result["tree-planted"].sort()).toEqual(tfProjectTrees); result = await service.getEstablishmentTrees("nurseries", nursery.uuid); expect(Object.keys(result).length).toBe(1); - expect(result["tree-planted"].sort()).toEqual(projectTreesPlanted); + expect(result["tree-planted"].sort()).toEqual(tfProjectTrees); result = await service.getEstablishmentTrees("site-reports", siteReport.uuid); expect(Object.keys(result).length).toBe(3); - expect(result["tree-planted"].sort()).toEqual(uniq([...siteTreesPlanted, ...projectTreesPlanted]).sort()); + expect(result["tree-planted"].sort()).toEqual(uniq([...siteTreesPlanted, ...tfProjectTrees]).sort()); expect(result["non-tree"].sort()).toEqual(siteNonTrees); expect(result["seeds"].sort()).toEqual(siteSeedings); result = await service.getEstablishmentTrees("nursery-reports", nurseryReport.uuid); expect(Object.keys(result).length).toBe(2); - expect(result["tree-planted"].sort()).toEqual(projectTreesPlanted); + expect(result["tree-planted"].sort()).toEqual(tfProjectTrees); expect(result["nursery-seedling"].sort()).toEqual(nurserySeedlings); }); @@ -168,25 +182,25 @@ describe("TreeService", () => { amount: (counts[tree.name]?.amount ?? 0) + (tree.amount ?? 0) } }); - const projectReportTreesPlanted = await TreeSpeciesFactory.forProjectReportTreePlanted.createMany(3, { + const projectReportTreesPlanted = await TreeSpeciesFactory.forProjectReportNurserySeedling.createMany(3, { speciesableId: projectReport1.id }); projectReportTreesPlanted.push( - await TreeSpeciesFactory.forProjectReportTreePlanted.create({ + await TreeSpeciesFactory.forProjectReportNurserySeedling.create({ speciesableId: projectReport1.id, taxonId: "wfo-projectreporttree" }) ); // hidden trees should be ignored - let hidden = await TreeSpeciesFactory.forProjectReportTreePlanted.create({ + let hidden = await TreeSpeciesFactory.forProjectReportNurserySeedling.create({ speciesableId: projectReport1.id, hidden: true }); let result = await service.getPreviousPlanting("project-reports", projectReport2.uuid); - expect(Object.keys(result)).toMatchObject(["tree-planted"]); - expect(result).toMatchObject({ "tree-planted": projectReportTreesPlanted.reduce(reduceTreeCounts, {}) }); - expect(Object.keys(result["tree-planted"])).not.toContain(hidden.name); + expect(Object.keys(result)).toMatchObject(["nursery-seedling"]); + expect(result).toMatchObject({ "nursery-seedling": projectReportTreesPlanted.reduce(reduceTreeCounts, {}) }); + expect(Object.keys(result["nursery-seedling"])).not.toContain(hidden.name); const siteReport1TreesPlanted = await TreeSpeciesFactory.forSiteReportTreePlanted.createMany(3, { speciesableId: siteReport1.id diff --git a/apps/entity-service/src/trees/tree.service.ts b/apps/entity-service/src/trees/tree.service.ts index f12d891c..dc64b373 100644 --- a/apps/entity-service/src/trees/tree.service.ts +++ b/apps/entity-service/src/trees/tree.service.ts @@ -10,7 +10,7 @@ import { } from "@terramatch-microservices/database/entities"; import { Includeable, Op, WhereOptions } from "sequelize"; import { BadRequestException, Injectable, NotFoundException } from "@nestjs/common"; -import { Dictionary, filter, flatten, flattenDeep, groupBy, uniq } from "lodash"; +import { Dictionary, filter, flatten, flattenDeep, groupBy, omit, uniq } from "lodash"; import { PreviousPlantingCountDto } from "./dto/establishment-trees.dto"; export const ESTABLISHMENT_REPORTS = ["project-reports", "site-reports", "nursery-reports"] as const; @@ -120,7 +120,7 @@ export class TreeService { // for these we simply pull the project's trees const whereOptions = { where: { uuid }, - attributes: [], + attributes: ["frameworkKey"], include: [ { model: Project, @@ -139,9 +139,21 @@ export class TreeService { : ProjectReport.findOne(whereOptions)); if (entityModel == null) throw new NotFoundException(); - return uniqueTreeNames( + const uniqueTrees = uniqueTreeNames( groupBy(flatten(Project.TREE_ASSOCIATIONS.map(association => entityModel.project[association])), "collection") ); + if (entity === "project-reports" && entityModel.frameworkKey === "ppc") { + // For PPC Project reports, we have to pretend the establishment species are "nursery-seedling" because + // that's the collection used at the report level, but "tree-planted" is used at the establishment level. + // The FE depends on the collection returned here to match what's being used in the tree species input + // or view table. + return { + ...omit(uniqueTrees, ["tree-planted"]), + ["nursery-seedling"]: uniqueTrees["tree-planted"] + }; + } + + return uniqueTrees; } else { throw new BadRequestException(`Entity type not supported: [${entity}]`); } diff --git a/apps/unified-database-service/src/airtable/entities/airtable-entity.spec.ts b/apps/unified-database-service/src/airtable/entities/airtable-entity.spec.ts index 7ec39682..dafefed3 100644 --- a/apps/unified-database-service/src/airtable/entities/airtable-entity.spec.ts +++ b/apps/unified-database-service/src/airtable/entities/airtable-entity.spec.ts @@ -603,10 +603,10 @@ describe("AirtableEntity", () => { }); allReports.push(ppcReport); const ppcSeedlings = ( - await TreeSpeciesFactory.forProjectReportTreePlanted.createMany(3, { speciesableId: ppcReport.id }) + await TreeSpeciesFactory.forProjectReportNurserySeedling.createMany(3, { speciesableId: ppcReport.id }) ).reduce((total, { amount }) => total + amount, 0); // make sure hidden is ignored - await TreeSpeciesFactory.forProjectReportTreePlanted.create({ speciesableId: ppcReport.id, hidden: true }); + await TreeSpeciesFactory.forProjectReportNurserySeedling.create({ speciesableId: ppcReport.id, hidden: true }); // TODO this might start causing problems when Task is implemented in this codebase and we have a factory // that's generating real records @@ -749,7 +749,7 @@ describe("AirtableEntity", () => { () => TreeSpeciesFactory.forNurserySeedling.create({ speciesableId: nursery.id }), () => TreeSpeciesFactory.forNurseryReportSeedling.create({ speciesableId: nurseryReport.id }), () => TreeSpeciesFactory.forProjectTreePlanted.create({ speciesableId: project.id }), - () => TreeSpeciesFactory.forProjectReportTreePlanted.create({ speciesableId: projectReport.id }), + () => TreeSpeciesFactory.forProjectReportNurserySeedling.create({ speciesableId: projectReport.id }), () => TreeSpeciesFactory.forSiteTreePlanted.create({ speciesableId: site.id }), () => TreeSpeciesFactory.forSiteNonTree.create({ speciesableId: site.id }), () => TreeSpeciesFactory.forSiteReportTreePlanted.create({ speciesableId: siteReport.id }), diff --git a/libs/database/src/lib/entities/nursery.entity.ts b/libs/database/src/lib/entities/nursery.entity.ts index 3a3bc0ad..ea7fbe3e 100644 --- a/libs/database/src/lib/entities/nursery.entity.ts +++ b/libs/database/src/lib/entities/nursery.entity.ts @@ -18,6 +18,7 @@ import { NurseryReport } from "./nursery-report.entity"; import { EntityStatus, UpdateRequestStatus } from "../constants/status"; import { chainScope } from "../util/chain-scope"; import { Subquery } from "../util/subquery.builder"; +import { FrameworkKey } from "../constants/framework"; // Incomplete stub @Scopes(() => ({ @@ -62,6 +63,10 @@ export class Nursery extends Model { @Column(STRING) name: string | null; + @AllowNull + @Column(STRING) + frameworkKey: FrameworkKey | null; + @ForeignKey(() => Project) @Column(BIGINT.UNSIGNED) projectId: number; diff --git a/libs/database/src/lib/factories/project-report.factory.ts b/libs/database/src/lib/factories/project-report.factory.ts index 72c80156..4dcc8be9 100644 --- a/libs/database/src/lib/factories/project-report.factory.ts +++ b/libs/database/src/lib/factories/project-report.factory.ts @@ -4,6 +4,7 @@ import { faker } from "@faker-js/faker"; import { ProjectFactory } from "./project.factory"; import { DateTime } from "luxon"; import { REPORT_STATUSES, UPDATE_REQUEST_STATUSES } from "../constants/status"; +import { FRAMEWORK_KEYS } from "../constants/framework"; export const ProjectReportFactory = FactoryGirl.define(ProjectReport, async () => { const dueAt = faker.date.past({ years: 2 }); @@ -11,6 +12,7 @@ export const ProjectReportFactory = FactoryGirl.define(ProjectReport, async () = return { uuid: crypto.randomUUID(), projectId: ProjectFactory.associate("id"), + frameworkKey: faker.helpers.arrayElement(FRAMEWORK_KEYS), dueAt, submittedAt: faker.date.between({ from: dueAt, to: DateTime.fromJSDate(dueAt).plus({ days: 14 }).toJSDate() }), status: faker.helpers.arrayElement(REPORT_STATUSES), diff --git a/libs/database/src/lib/factories/tree-species.factory.ts b/libs/database/src/lib/factories/tree-species.factory.ts index e19643db..2bc46dd3 100644 --- a/libs/database/src/lib/factories/tree-species.factory.ts +++ b/libs/database/src/lib/factories/tree-species.factory.ts @@ -39,11 +39,11 @@ export const TreeSpeciesFactory = { collection: "tree-planted" })), - forProjectReportTreePlanted: FactoryGirl.define(TreeSpecies, async () => ({ + forProjectReportNurserySeedling: FactoryGirl.define(TreeSpecies, async () => ({ ...(await defaultAttributesFactory()), speciesableType: ProjectReport.LARAVEL_TYPE, speciesableId: ProjectReportFactory.associate("id"), - collection: "tree-planted" + collection: "nursery-seedling" })), forSiteTreePlanted: FactoryGirl.define(TreeSpecies, async () => ({ From 71bda4fdacae96baa5438d29e8c2e4bf240ce2cb Mon Sep 17 00:00:00 2001 From: Jose Carlos Laura Ramirez Date: Fri, 28 Feb 2025 20:20:52 -0400 Subject: [PATCH 5/5] [TM-1759] enable cors on research service --- apps/research-service/src/main.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/apps/research-service/src/main.ts b/apps/research-service/src/main.ts index 325dc254..56e2dd5e 100644 --- a/apps/research-service/src/main.ts +++ b/apps/research-service/src/main.ts @@ -13,6 +13,11 @@ async function bootstrap() { const app = await NestFactory.create(AppModule); app.set("query parser", "extended"); + if (process.env.NODE_ENV === "development") { + // CORS is handled by the Api Gateway in AWS + app.enableCors(); + } + const config = new DocumentBuilder() .setTitle("TerraMatch Research Service") .setDescription("APIs related to needs for the data research team.")