Skip to content

Commit

Permalink
Merge pull request #5964 from beyondessential/release-2024-43
Browse files Browse the repository at this point in the history
Release 2024-43
  • Loading branch information
avaek authored Oct 21, 2024
2 parents 6efd7a1 + bca1e71 commit 7408f7a
Show file tree
Hide file tree
Showing 91 changed files with 1,365 additions and 289 deletions.
1 change: 1 addition & 0 deletions packages/auth/src/Authenticator.js
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,7 @@ export class Authenticator {
user_id: user.id,
platform,
app_version: appVersion,
last_login: new Date(),
},
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,15 @@ import { testAuthenticateRefreshToken } from './testAuthenticateRefreshToken';
jest.mock('rand-token');
randomToken.generate.mockReturnValue(refreshToken);

beforeAll(() => {
jest.useFakeTimers('modern');
jest.setSystemTime(new Date(2020, 3, 1));
});

afterAll(() => {
jest.useRealTimers();
});

describe('Authenticator', () => {
describe('authenticatePassword', testAuthenticatePassword);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,7 @@ export const testAuthenticatePassword = () => {
user_id: verifiedUser.id,
app_version: meditrakDeviceDetails.appVersion,
platform: meditrakDeviceDetails.platform,
last_login: new Date(),
},
);

Expand Down
1 change: 1 addition & 0 deletions packages/auth/src/userAuth.js
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ export const getAuthorizationObject = async ({
email: user.email,
profileImage: user.profile_image,
verifiedEmail: user.verified_email,
preferences: user.preferences,
accessPolicy,
};
if (permissionGroups) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ export async function updateCountryEntities(
countryCode,
pushToDhis,
);

await transactingModels.entity.findOrCreate(
{ code: countryCode },
{
Expand All @@ -103,6 +104,7 @@ export async function updateCountryEntities(
},
);
const codes = []; // An array to hold all facility codes, allowing duplicate checking

for (let i = 0; i < entityObjects.length; i++) {
const entityObject = entityObjects[i];
const { entity_type: entityType } = entityObject;
Expand Down Expand Up @@ -192,7 +194,27 @@ export async function updateCountryEntities(
geojson.type === 'Polygon'
? { type: 'MultiPolygon', coordinates: [geojson.coordinates] }
: geojson;
await transactingModels.entity.updateRegionCoordinates(code, translatedGeojson);

try {
await transactingModels.entity.updateRegionCoordinates(code, translatedGeojson);
} catch (error) {
if (error.message.includes('payload string too long')) {
const largeGeoEntities = entityObjects.filter(entityObject => {
if (!entityObject?.geojson) return false;
const geoJsonString = JSON.stringify(entityObject.geojson);
// If the geo json is too large, we will hit the max payload size limit.
// Hard postgres max is 8000 characters, but we need to account for other data in the query payload
const maxGeoJsonPayload = 5200;
if (geoJsonString.length > maxGeoJsonPayload) {
return true;
}
});
const text = largeGeoEntities.map(entity => entity.code).join(', ');
error.message = `Error updating region coordinates for entities: ${text} ${error.message}`;
}

throw error;
}
}
}
return country;
Expand Down
9 changes: 8 additions & 1 deletion packages/database/src/TupaiaDatabase.js
Original file line number Diff line number Diff line change
Expand Up @@ -662,7 +662,8 @@ function addWhereClause(connection, baseQuery, where) {
return querySoFar; // Ignore undefined criteria
}
if (value === null) {
return querySoFar.whereNull(key);
const columnKey = getColSelector(connection, key);
return querySoFar.whereNull(columnKey);
}
const {
comparisonType = 'where',
Expand Down Expand Up @@ -748,5 +749,11 @@ function getColSelector(connection, inputColStr) {
return connection.raw(inputColStr);
}

const asGeoJsonPattern = /^ST_AsGeoJSON\((.+)\)$/;
if (asGeoJsonPattern.test(inputColStr)) {
const [, argsString] = inputColStr.match(asGeoJsonPattern);
return connection.raw(`ST_AsGeoJSON(${argsString})`);
}

return inputColStr;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/*
* Tupaia
* Copyright (c) 2017 - 2024 Beyond Essential Systems Pty Ltd
*/
import { getLeaderboard } from '../../modelClasses/SurveyResponse';

const USERS_EXCLUDED_FROM_LEADER_BOARD = [
"'edmofro@gmail.com'",
"'kahlinda.mahoney@gmail.com'",
"'lparish1980@gmail.com'",
"'sus.lake@gmail.com'",
"'michaelnunan@hotmail.com'",
"'vanbeekandrew@gmail.com'",
"'gerardckelly@gmail.com'",
"'geoffreyfisher@hotmail.com'",
"'unicef.laos.edu@gmail.com'",
];
const SYSTEM_USERS = ["'tamanu-server@tupaia.org'", "'public@tupaia.org'", "'josh@sussol.net'"];

const whitespace = /\s/g;
const expectToBe = (expected, received) => {
expect(received.replace(whitespace, '')).toBe(expected.replace(whitespace, ''));
};

describe('getLeaderboard()', () => {
it('should filter out internal users on standard projects', async () => {
const expectedLeaderboard = `SELECT r.user_id, user_account.first_name, user_account.last_name, r.coconuts, r.pigs
FROM (
SELECT user_id, FLOOR(COUNT(*)) as coconuts, FLOOR(COUNT(*) / 100) as pigs
FROM survey_response
JOIN survey on survey.id=survey_id
WHERE survey.project_id = ?
GROUP BY user_id
) r
JOIN user_account on user_account.id = r.user_id
WHERE email NOT IN (${[...SYSTEM_USERS, ...USERS_EXCLUDED_FROM_LEADER_BOARD].join(', ')})
AND email NOT LIKE '%@beyondessential.com.au' AND email NOT LIKE '%@bes.au'
ORDER BY coconuts DESC
LIMIT ?;`;

expectToBe(getLeaderboard('5dfc6eaf61f76a497716cddf'), expectedLeaderboard);
});

it('should not filter out internal users on internal projects', async () => {
const INTERNAL_PROJECT_IDS = [
'6684ac9d0f018e110b000a00', // bes_asset_demo
'66a03660718c54751609eeed', // bes_asset_tracker
'6704622a45a4fc4941071605', // bes_reporting
];
const expectedLeaderboard = `SELECT r.user_id, user_account.first_name, user_account.last_name, r.coconuts, r.pigs
FROM (
SELECT user_id, FLOOR(COUNT(*)) as coconuts, FLOOR(COUNT(*) / 100) as pigs
FROM survey_response
JOIN survey on survey.id=survey_id
WHERE survey.project_id = ?
GROUP BY user_id
) r
JOIN user_account on user_account.id = r.user_id
WHERE email NOT IN (${SYSTEM_USERS.join(', ')})
ORDER BY coconuts DESC
LIMIT ?;`;

INTERNAL_PROJECT_IDS.forEach(projectId => {
expectToBe(getLeaderboard(projectId), expectedLeaderboard);
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
'use strict';

var dbm;
var type;
var seed;

/**
* We receive the dbmigrate dependency from dbmigrate initially.
* This enables us to not have to rely on NODE_PATH.
*/
exports.setup = function (options, seedLink) {
dbm = options.dbmigrate;
type = dbm.dataType;
seed = seedLink;
};

exports.up = function (db) {
return db.runSql(`
ALTER TABLE meditrak_device
ADD COLUMN last_login TIMESTAMP;
`);
};

exports.down = function (db) {
return db.runSql(`
ALTER TABLE meditrak_device DROP COLUMN last_login;
`);
};

exports._meta = {
version: 1,
};
54 changes: 35 additions & 19 deletions packages/database/src/modelClasses/SurveyResponse.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,44 @@ const USERS_EXCLUDED_FROM_LEADER_BOARD = [
"'vanbeekandrew@gmail.com'", // Andrew
"'gerardckelly@gmail.com'", // Gerry K
"'geoffreyfisher@hotmail.com'", // Geoff F
"'josh@sussol.net'", // mSupply API Client
"'unicef.laos.edu@gmail.com'", // Laos Schools Data Collector
];
const SYSTEM_USERS = [
"'tamanu-server@tupaia.org'", // Tamanu Server
"'public@tupaia.org'", // Public User
"'josh@sussol.net'", // mSupply API Client
];
const INTERNAL_EMAIL = ['@beyondessential.com.au', '@bes.au'];
const INTERNAL_PROJECT_IDS = [
'6684ac9d0f018e110b000a00', // bes_asset_demo
'66a03660718c54751609eeed', // bes_asset_tracker
'6704622a45a4fc4941071605', // bes_reporting
];

export function getLeaderboard(projectId = '') {
const isInternalProject = projectId && INTERNAL_PROJECT_IDS.includes(projectId);

const besUsersFilter = `AND ${INTERNAL_EMAIL.map(email => `email NOT LIKE '%${email}'`).join(' AND ')}`;
const excludedUserAccountList = isInternalProject
? SYSTEM_USERS
: [...SYSTEM_USERS, ...USERS_EXCLUDED_FROM_LEADER_BOARD];

// FLOOR to force result to be returned as int, not string
return `SELECT r.user_id, user_account.first_name, user_account.last_name, r.coconuts, r.pigs
FROM (
SELECT user_id, FLOOR(COUNT(*)) as coconuts, FLOOR(COUNT(*) / 100) as pigs
FROM survey_response
JOIN survey on survey.id=survey_id
${projectId ? 'WHERE survey.project_id = ?' : ''}
GROUP BY user_id
) r
JOIN user_account on user_account.id = r.user_id
WHERE email NOT IN (${excludedUserAccountList.join(',')})
${!isInternalProject ? besUsersFilter : ''}
ORDER BY coconuts DESC
LIMIT ?;
`;
}

export class SurveyResponseRecord extends DatabaseRecord {
static databaseRecord = RECORDS.SURVEY_RESPONSE;
Expand All @@ -38,23 +70,7 @@ export class SurveyResponseModel extends MaterializedViewLogDatabaseModel {

async getLeaderboard(projectId = '', rowCount = 10) {
const bindings = projectId ? [projectId, rowCount] : [rowCount];
return this.database.executeSql(
`SELECT r.user_id, user_account.first_name, user_account.last_name, r.coconuts, r.pigs
FROM (
SELECT user_id, FLOOR(COUNT(*)) as coconuts, FLOOR(COUNT(*) / 100) as pigs
-- ^~~~~~~~~~~~~~~ FLOOR to force result to be returned as int, not string
FROM survey_response
JOIN survey on survey.id=survey_id
${projectId ? 'WHERE survey.project_id = ?' : ''}
GROUP BY user_id
) r
JOIN user_account on user_account.id = r.user_id
WHERE ${INTERNAL_EMAIL.map(email => `email NOT LIKE '%${email}'`).join(' AND ')}
AND email NOT IN (${USERS_EXCLUDED_FROM_LEADER_BOARD.join(',')})
ORDER BY coconuts DESC
LIMIT ?;
`,
bindings,
);
const query = getLeaderboard(projectId);
return this.database.executeSql(query, bindings);
}
}
26 changes: 24 additions & 2 deletions packages/datatrak-web/src/api/queries/useProjects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,28 @@ import { useQuery } from '@tanstack/react-query';
import { DatatrakWebProjectsRequest } from '@tupaia/types';
import { get } from '../api';

export const useProjects = () => {
return useQuery(['projects'], (): Promise<DatatrakWebProjectsRequest.ResBody> => get('projects'));
export const useProjects = (sortByAccess = true) => {
const { data, ...query } = useQuery(
['projects'],
(): Promise<DatatrakWebProjectsRequest.ResBody> => get('projects'),
);

if (data && sortByAccess) {
data.sort((a, b) => {
// Sort by hasAccess = true first
if (a.hasAccess !== b.hasAccess) {
return a.hasAccess ? -1 : 1;
}

// Sort by hasPendingAccess = true second
if (a.hasPendingAccess !== b.hasPendingAccess) {
return a.hasPendingAccess ? -1 : 1;
}

// Otherwise, sort alphabetically by name
return a.name.localeCompare(b.name);
});
}

return { ...query, data };
};
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ export const ChangeProjectButton = ({ className }: { className?: string }) => {
<ProjectButton onClick={openProjectModal} tooltip="Change project">
{projectName ?? 'Select project'}
</ProjectButton>
{projectModalIsOpen && <ProjectSelectModal onClose={closeProjectModal} />}
{projectModalIsOpen && <ProjectSelectModal onBack={closeProjectModal} />}
</Container>
);
};
7 changes: 0 additions & 7 deletions packages/datatrak-web/src/components/SelectList/index.ts

This file was deleted.

1 change: 0 additions & 1 deletion packages/datatrak-web/src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@

export { PageContainer } from './PageContainer';
export * from './Icons';
export * from './SelectList';
export { Autocomplete, QuestionAutocomplete } from './Autocomplete';
export { Button } from './Button';
export { ButtonLink } from './ButtonLink';
Expand Down
20 changes: 17 additions & 3 deletions packages/datatrak-web/src/features/EntitySelector/ResultsList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@
* Copyright (c) 2017 - 2023 Beyond Essential Systems Pty Ltd
*/

import React from 'react';
import React, { ReactNode } from 'react';
import styled from 'styled-components';
import { Typography } from '@material-ui/core';
import { FormLabelProps, Typography } from '@material-ui/core';
import RoomIcon from '@material-ui/icons/Room';
import { DatatrakWebEntityDescendantsRequest } from '@tupaia/types';
import { ListItemType, SelectList } from '../../components';
import { SelectList } from '@tupaia/ui-components';

const DARK_BLUE = '#004975';

Expand Down Expand Up @@ -43,6 +43,20 @@ export const ResultItem = ({ name, parentName }) => {
);
};

type ListItemType = Record<string, unknown> & {
children?: ListItemType[];
content: string | ReactNode;
value: string;
selected?: boolean;
icon?: ReactNode;
tooltip?: string;
button?: boolean;
disabled?: boolean;
labelProps?: FormLabelProps & {
component?: React.ElementType;
};
};

type SearchResults = DatatrakWebEntityDescendantsRequest.ResBody;
interface ResultsListProps {
value: string;
Expand Down
Loading

0 comments on commit 7408f7a

Please sign in to comment.