Skip to content

Commit

Permalink
Merge pull request #3127 from LiteFarmOrg/LF-4090/Display_KPIs_in_das…
Browse files Browse the repository at this point in the history
…hboard

LF-4090 (backend): Display KPIs in dashboard
  • Loading branch information
antsgar authored Feb 21, 2024
2 parents 314ac33 + 68bdad8 commit 98390d9
Show file tree
Hide file tree
Showing 6 changed files with 329 additions and 25 deletions.
6 changes: 5 additions & 1 deletion packages/api/src/controllers/customAnimalTypeController.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,11 @@ const customAnimalTypeController = {
return async (req, res) => {
try {
const { farm_id } = req.headers;
const rows = await CustomAnimalTypeModel.query().where({ farm_id }).whereNotDeleted();
const rows =
req.query.count === 'true'
? await CustomAnimalTypeModel.getCustomAnimalTypesWithCountsByFarmId(farm_id)
: await CustomAnimalTypeModel.query().where({ farm_id }).whereNotDeleted();

return res.status(200).send(rows);
} catch (error) {
console.error(error);
Expand Down
11 changes: 8 additions & 3 deletions packages/api/src/controllers/defaultAnimalTypeController.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,17 @@ const defaultAnimalTypeController = {
getDefaultAnimalTypes() {
return async (req, res) => {
try {
const rows = await DefaultAnimalTypeModel.query();
const { farm_id } = req.headers;
const rows =
req.query.count === 'true'
? await DefaultAnimalTypeModel.getDefaultAnimalTypesWithCountsByFarmId(farm_id)
: await DefaultAnimalTypeModel.query();

if (!rows.length) {
return res.sendStatus(404);
} else {
return res.status(200).send(rows);
}

return res.status(200).send(rows);
} catch (error) {
console.error(error);
return res.status(500).json({
Expand Down
24 changes: 24 additions & 0 deletions packages/api/src/models/customAnimalTypeModel.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
*/

import baseModel from './baseModel.js';
import knex from '../util/knex.js';

class CustomAnimalType extends baseModel {
static get tableName() {
Expand Down Expand Up @@ -41,6 +42,29 @@ class CustomAnimalType extends baseModel {
additionalProperties: false,
};
}

static async getCustomAnimalTypesWithCountsByFarmId(farm_id) {
const data = await knex.raw(
`SELECT
cat.*,
COALESCE(SUM(abu.count), 0) AS count
FROM
custom_animal_type AS cat
LEFT JOIN (
SELECT custom_type_id, COUNT(*) AS count
FROM animal WHERE farm_id = ? AND deleted is FALSE
GROUP BY custom_type_id
UNION ALL
SELECT custom_type_id, SUM(count) AS count
FROM animal_batch WHERE farm_id = ? AND deleted is FALSE
GROUP BY custom_type_id
) AS abu ON cat.id = abu.custom_type_id
WHERE farm_id = ? AND deleted is FALSE
GROUP BY cat.id;`,
[farm_id, farm_id, farm_id],
);
return data.rows;
}
}

export default CustomAnimalType;
25 changes: 25 additions & 0 deletions packages/api/src/models/defaultAnimalTypeModel.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
*/

import Model from './baseFormatModel.js';
import knex from '../util/knex.js';

class DefaultAnimalTypeModel extends Model {
static get tableName() {
Expand All @@ -39,6 +40,30 @@ class DefaultAnimalTypeModel extends Model {
additionalProperties: false,
};
}

static async getDefaultAnimalTypesWithCountsByFarmId(farm_id) {
const data = await knex.raw(
`SELECT
dat.*,
COALESCE(SUM(abu.count), 0) AS count
FROM
default_animal_type AS dat
LEFT JOIN (
SELECT default_type_id, COUNT(*) AS count
FROM animal
WHERE farm_id = ? AND deleted is FALSE
GROUP BY default_type_id
UNION ALL
SELECT default_type_id, SUM(count) AS count
FROM animal_batch
WHERE farm_id = ? AND deleted is FALSE
GROUP BY default_type_id
) AS abu ON dat.id = abu.default_type_id
GROUP BY dat.id;`,
[farm_id, farm_id],
);
return data.rows;
}
}

export default DefaultAnimalTypeModel;
151 changes: 141 additions & 10 deletions packages/api/tests/custom_animal_type.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,17 +48,14 @@ describe('Custom Animal Type Tests', () => {
done();
});

function getRequest({ user_id, farm_id }, callback) {
chai
async function getRequest({ user_id, farm_id }, query = '') {
return await chai
.request(server)
.get('/custom_animal_types')
.get(`/custom_animal_types${query}`)
.set('user_id', user_id)
.set('farm_id', farm_id)
.end(callback);
.set('farm_id', farm_id);
}

const getRequestAsPromise = util.promisify(getRequest);

function postRequest(data, { user_id, farm_id }, callback) {
chai
.request(server)
Expand Down Expand Up @@ -90,9 +87,10 @@ describe('Custom Animal Type Tests', () => {
return { mainFarm, user };
}

async function makeUserCreatedAnimalType(mainFarm) {
async function makeUserCreatedAnimalType(mainFarm, properties = {}) {
const [custom_animal_type] = await mocks.custom_animal_typeFactory({
promisedFarm: [mainFarm],
properties,
});
return custom_animal_type;
}
Expand All @@ -106,7 +104,7 @@ describe('Custom Animal Type Tests', () => {
const { mainFarm, user } = await returnUserFarms(role);
const custom_animal_type = await makeUserCreatedAnimalType(mainFarm);

const res = await getRequestAsPromise({ user_id: user.user_id, farm_id: mainFarm.farm_id });
const res = await getRequest({ user_id: user.user_id, farm_id: mainFarm.farm_id });

expect(res.status).toBe(200);
res.body.forEach((type) => {
Expand All @@ -120,7 +118,7 @@ describe('Custom Animal Type Tests', () => {
await makeUserCreatedAnimalType(mainFarm);
const [unAuthorizedUser] = await mocks.usersFactory();

const res = await getRequestAsPromise({
const res = await getRequest({
user_id: unAuthorizedUser.user_id,
farm_id: mainFarm.farm_id,
});
Expand All @@ -129,6 +127,139 @@ describe('Custom Animal Type Tests', () => {
'User does not have the following permission(s): get:animal_types',
);
});

test(`Should not get other farms' types`, async () => {
const { mainFarm, user } = await returnUserFarms(1);
const [secondFarm] = await mocks.farmFactory();

// make types for the farm and another farm
await makeUserCreatedAnimalType(mainFarm);
await makeUserCreatedAnimalType(secondFarm);

const resWithoutCount = await getRequest({
user_id: user.user_id,
farm_id: mainFarm.farm_id,
});

const resWithCount = await getRequest(
{
user_id: user.user_id,
farm_id: mainFarm.farm_id,
},
'?count=true',
);

expect(resWithoutCount.body.length).toBe(1);
expect(resWithCount.body.length).toBe(1);
});

test('Should not get deleted types', async () => {
const { mainFarm, user } = await returnUserFarms(1);
// make an active type and a deleted type
await makeUserCreatedAnimalType(mainFarm);
await makeUserCreatedAnimalType(mainFarm, { deleted: true });

const resWithoutCount = await getRequest({
user_id: user.user_id,
farm_id: mainFarm.farm_id,
});

const resWithCount = await getRequest(
{
user_id: user.user_id,
farm_id: mainFarm.farm_id,
},
'?count=true',
);

expect(resWithoutCount.body.length).toBe(1);
expect(resWithCount.body.length).toBe(1);
});

test('Should get counts with count=true query', async () => {
const { mainFarm, user } = await returnUserFarms(1);

// typeId will be added later
const animalBatchTypeCounts = [
{ animalCount: 0, batchCounts: [] },
{ animalCount: 1, batchCounts: [] },
{ animalCount: 0, batchCounts: [2, 40] },
{ animalCount: 11, batchCounts: [20, 40, 60, 80, 100] },
];

// make animals and batches
for (const animalBatchTypeCount of animalBatchTypeCounts) {
const { animalCount, batchCounts } = animalBatchTypeCount;
const type = await makeUserCreatedAnimalType(mainFarm);

// add type to animalBatchTypeCounts for validation later
animalBatchTypeCount.typeId = type.id;

for (let i = 0; i < animalCount; i++) {
await mocks.animalFactory({
promisedFarm: [mainFarm],
promisedDefaultAnimalType: [() => ({})],
properties: { custom_type_id: type.id },
});
}
for (const batchCount of batchCounts) {
await mocks.animal_batchFactory({
promisedFarm: [mainFarm],
promisedDefaultAnimalType: [() => ({})],
promisedDefaultAnimalBreed: [() => ({})],
properties: { custom_type_id: type.id, count: batchCount },
});
}
}

const res = await getRequest(
{
user_id: user.user_id,
farm_id: mainFarm.farm_id,
},
'?count=true',
);

res.body.forEach(({ id, count }) => {
const expectedCountsData = animalBatchTypeCounts.find(({ typeId }) => id === typeId);
const expectedCount =
expectedCountsData.animalCount +
expectedCountsData.batchCounts.reduce((acc, currentValue) => acc + currentValue, 0);

expect(count).toBe(expectedCount);
});
});

test('Deleted animals or batches should not be counted', async () => {
const { mainFarm, user } = await returnUserFarms(1);
const type = await makeUserCreatedAnimalType(mainFarm);

// make active animal, active batch(50), deleted animal and deleted batch(50)
for (let i = 0; i < 2; i++) {
await mocks.animalFactory({
promisedFarm: [mainFarm],
promisedDefaultAnimalType: [() => ({})],
properties: { custom_type_id: type.id, deleted: i % 2 === 0 },
});

await mocks.animal_batchFactory({
promisedFarm: [mainFarm],
promisedDefaultAnimalType: [() => ({})],
promisedDefaultAnimalBreed: [() => ({})],
properties: { custom_type_id: type.id, count: 50, deleted: i % 2 === 0 },
});
}

const res = await getRequest(
{
user_id: user.user_id,
farm_id: mainFarm.farm_id,
},
'?count=true',
);

expect(res.body[0].count).toBe(51);
});
});

// POST tests
Expand Down
Loading

0 comments on commit 98390d9

Please sign in to comment.