Skip to content

Commit

Permalink
Merge branch 'staging' into TM-1766_entity_get_and_index_site
Browse files Browse the repository at this point in the history
  • Loading branch information
pachonjcl committed Mar 1, 2025
2 parents 1278b04 + 54309d0 commit 32d2a9d
Show file tree
Hide file tree
Showing 9 changed files with 70 additions and 30 deletions.
52 changes: 33 additions & 19 deletions apps/entity-service/src/trees/tree.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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);
});

Expand Down Expand Up @@ -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
Expand Down
18 changes: 15 additions & 3 deletions apps/entity-service/src/trees/tree.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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,
Expand All @@ -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}]`);
}
Expand Down
5 changes: 5 additions & 0 deletions apps/research-service/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@ async function bootstrap() {
const app = await NestFactory.create<NestExpressApplication>(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.")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,8 @@ export class SitePolygonQueryBuilder extends PaginatedQueryBuilder<SitePolygon>
{ model: PolygonGeometry, attributes: ["polygon"], required: true },
this.siteJoin
];

this.where({ isActive: true });
}

async excludeTestProjects() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 }),
Expand Down
5 changes: 5 additions & 0 deletions libs/database/src/lib/entities/nursery.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(() => ({
Expand Down Expand Up @@ -62,6 +63,10 @@ export class Nursery extends Model<Nursery> {
@Column(STRING)
name: string | null;

@AllowNull
@Column(STRING)
frameworkKey: FrameworkKey | null;

@ForeignKey(() => Project)
@Column(BIGINT.UNSIGNED)
projectId: number;
Expand Down
6 changes: 3 additions & 3 deletions libs/database/src/lib/entities/project-report.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ type ApprovedIdsSubqueryOptions = {
}))
@Table({ tableName: "v2_project_reports", underscored: true, paranoid: true })
export class ProjectReport extends Model<ProjectReport> {
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";
Expand Down Expand Up @@ -250,7 +250,7 @@ export class ProjectReport extends Model<ProjectReport> {
@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;
nurserySeedlings: TreeSpecies[] | null;
}
2 changes: 2 additions & 0 deletions libs/database/src/lib/factories/project-report.factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,15 @@ 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 });
dueAt.setMilliseconds(0);
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),
Expand Down
4 changes: 2 additions & 2 deletions libs/database/src/lib/factories/tree-species.factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => ({
Expand Down

0 comments on commit 32d2a9d

Please sign in to comment.