From 747f182383002b66fd14011e2a175db039c4d282 Mon Sep 17 00:00:00 2001 From: acdunham Date: Fri, 3 Nov 2023 08:20:18 +1300 Subject: [PATCH] Update to use new leaderboard model --- packages/central-server/src/apiV2/index.js | 2 -- .../apiV2/meditrakApp/getLeaderboardList.js | 18 ---------- .../src/apiV2/meditrakApp/index.js | 1 - .../src/modelClasses/SurveyResponse.js | 33 +++++++++++++++++++ .../datatrak-web-server/src/app/createApp.ts | 3 ++ .../src/models/SurveyResponse.ts | 30 +++++++++++++++++ .../datatrak-web-server/src/models/index.ts | 1 + .../src/routes/LeaderboardRoute.ts | 25 ++++++++++++++ .../datatrak-web-server/src/routes/index.ts | 1 + packages/datatrak-web-server/src/types.ts | 2 ++ .../src/api/queries/useLeaderboard.ts | 7 ++-- .../features/Leaderboard/LeaderboardTable.tsx | 4 +-- .../socialFeed/SocialFeedRoute.test.ts | 4 ++- .../src/routes/SocialFeedRoute.ts | 4 +-- .../datatrak-web-server/LeaderboardRequest.ts | 12 +++++++ .../requests/datatrak-web-server/index.ts | 1 + packages/types/src/types/requests/index.ts | 1 + 17 files changed, 121 insertions(+), 28 deletions(-) delete mode 100644 packages/central-server/src/apiV2/meditrakApp/getLeaderboardList.js create mode 100644 packages/datatrak-web-server/src/models/SurveyResponse.ts create mode 100644 packages/datatrak-web-server/src/routes/LeaderboardRoute.ts create mode 100644 packages/types/src/types/requests/datatrak-web-server/LeaderboardRequest.ts diff --git a/packages/central-server/src/apiV2/index.js b/packages/central-server/src/apiV2/index.js index daf61f2a7d..78072432b1 100644 --- a/packages/central-server/src/apiV2/index.js +++ b/packages/central-server/src/apiV2/index.js @@ -20,7 +20,6 @@ import { getSocialFeed, getUserRewards, postChanges, - getLeaderboardList, } from './meditrakApp'; import { BESAdminCreateHandler } from './CreateHandler'; import { BESAdminDeleteHandler } from './DeleteHandler'; @@ -165,7 +164,6 @@ apiV2.get('/changes/count', catchAsyncErrors(countChanges)); apiV2.get('/changes/metadata', catchAsyncErrors(changesMetadata)); apiV2.get('/changes', catchAsyncErrors(getChanges)); apiV2.get('/downloadFiles', useRouteHandler(DownloadFiles)); -apiV2.get('/leaderboard', catchAsyncErrors(getLeaderboardList)); apiV2.get('/socialFeed', catchAsyncErrors(getSocialFeed)); apiV2.get('/me', useRouteHandler(GETUserForMe)); apiV2.get('/me/rewards', allowAnyone(getUserRewards)); diff --git a/packages/central-server/src/apiV2/meditrakApp/getLeaderboardList.js b/packages/central-server/src/apiV2/meditrakApp/getLeaderboardList.js deleted file mode 100644 index 6705f4e19f..0000000000 --- a/packages/central-server/src/apiV2/meditrakApp/getLeaderboardList.js +++ /dev/null @@ -1,18 +0,0 @@ -/** - * Tupaia - * Copyright (c) 2017 - 2023 Beyond Essential Systems Pty Ltd - */ - -import { respond } from '@tupaia/utils'; -import { getLeaderboard } from '../../social'; -import { allowNoPermissions } from '../../permissions'; - -export const getLeaderboardList = async (req, res) => { - const { query, models } = req; - - await req.assertPermissions(allowNoPermissions); - - const leaderboard = await getLeaderboard(models, query?.rowCount); - - respond(res, leaderboard); -}; diff --git a/packages/central-server/src/apiV2/meditrakApp/index.js b/packages/central-server/src/apiV2/meditrakApp/index.js index b3171eb31f..b761889330 100644 --- a/packages/central-server/src/apiV2/meditrakApp/index.js +++ b/packages/central-server/src/apiV2/meditrakApp/index.js @@ -9,4 +9,3 @@ export { getSocialFeed } from './getSocialFeed'; export { getUserRewards } from './getUserRewards'; export { postChanges } from './postChanges'; export * from './countChanges'; -export { getLeaderboardList } from './getLeaderboardList'; diff --git a/packages/database/src/modelClasses/SurveyResponse.js b/packages/database/src/modelClasses/SurveyResponse.js index c2013d147c..6975b3ec3d 100644 --- a/packages/database/src/modelClasses/SurveyResponse.js +++ b/packages/database/src/modelClasses/SurveyResponse.js @@ -7,6 +7,21 @@ import { MaterializedViewLogDatabaseModel } from '../analytics'; import { DatabaseType } from '../DatabaseType'; import { TYPES } from '../types'; +const EXCLUDED_USERS = [ + "'edmofro@gmail.com'", // Edwin + "'kahlinda.mahoney@gmail.com'", // Kahlinda + "'lparish1980@gmail.com'", // Lewis + "'sus.lake@gmail.com'", // Susie + "'michaelnunan@hotmail.com'", // Michael + "'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 + "'tamanu-server@tupaia.org'", // Tamanu Server +]; +const INTERNAL_EMAIL = ['@beyondessential.com.au', '@bes.au']; + export class SurveyResponseType extends DatabaseType { static databaseType = TYPES.SURVEY_RESPONSE; } @@ -15,4 +30,22 @@ export class SurveyResponseModel extends MaterializedViewLogDatabaseModel { get DatabaseTypeClass() { return SurveyResponseType; } + + async getLeaderboard(rowCount = 10) { + return this.database.executeSql( + ` SELECT r.user_id, user_account.first_name, user_account.last_name, r.coconuts, r.pigs + FROM ( + SELECT user_id, COUNT(*) as coconuts, FLOOR(COUNT(*) / 100) as pigs + FROM survey_response + 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 (${EXCLUDED_USERS.join(',')}) + ORDER BY coconuts DESC + LIMIT ?; + `, + [rowCount], + ); + } } diff --git a/packages/datatrak-web-server/src/app/createApp.ts b/packages/datatrak-web-server/src/app/createApp.ts index 09b5d83a72..4daeb974e0 100644 --- a/packages/datatrak-web-server/src/app/createApp.ts +++ b/packages/datatrak-web-server/src/app/createApp.ts @@ -30,6 +30,8 @@ import { ProjectRoute, SubmitSurveyRoute, SubmitSurveyRequest, + LeaderboardRequest, + LeaderboardRoute, } from '../routes'; const { @@ -53,6 +55,7 @@ export function createApp() { .get('surveyResponses', handleWith(SurveyResponsesRoute)) .get('surveys/:surveyCode', handleWith(SurveyRoute)) .get('projects', handleWith(ProjectsRoute)) + .get('leaderboard', handleWith(LeaderboardRoute)) .get('project/:projectCode', handleWith(ProjectRoute)) .use('signup', forwardRequest(WEB_CONFIG_API_URL, { authHandlerProvider })) // Forward everything else to central server diff --git a/packages/datatrak-web-server/src/models/SurveyResponse.ts b/packages/datatrak-web-server/src/models/SurveyResponse.ts new file mode 100644 index 0000000000..aede873fe0 --- /dev/null +++ b/packages/datatrak-web-server/src/models/SurveyResponse.ts @@ -0,0 +1,30 @@ +/** + * Tupaia + * Copyright (c) 2017 - 2023 Beyond Essential Systems Pty Ltd + */ +import { + SurveyResponseModel as BaseSurveyResponseModel, + SurveyResponseType as BaseSurveyResponseType, +} from '@tupaia/database'; +import { Model } from '@tupaia/server-boilerplate'; + +export type SurveyResponseModelFields = Readonly<{ + id: string; + survey_id: string; + user_id: string; + start_time: string; + end_time: string; + metadata: Record; + timezone: string | null; + entity_id: string; + data_time: string | null; + outdated: boolean | null; + approval_status: string; +}>; + +export interface SurveyResponseModelType + extends SurveyResponseModelFields, + Omit {} // Omit base `id: any` type as we explicity define as a string here + +export interface SurveyResponseModel + extends Model {} diff --git a/packages/datatrak-web-server/src/models/index.ts b/packages/datatrak-web-server/src/models/index.ts index 8d59216755..e5c6e7bfae 100644 --- a/packages/datatrak-web-server/src/models/index.ts +++ b/packages/datatrak-web-server/src/models/index.ts @@ -5,3 +5,4 @@ export { DataTrakSessionModel, DataTrakSessionType } from './DataTrakSession'; export { EntityModel } from './Entity'; +export { SurveyResponseModel } from './SurveyResponse'; diff --git a/packages/datatrak-web-server/src/routes/LeaderboardRoute.ts b/packages/datatrak-web-server/src/routes/LeaderboardRoute.ts new file mode 100644 index 0000000000..c6052f7fb0 --- /dev/null +++ b/packages/datatrak-web-server/src/routes/LeaderboardRoute.ts @@ -0,0 +1,25 @@ +/** + * Tupaia + * Copyright (c) 2017 - 2023 Beyond Essential Systems Pty Ltd + */ + +import { Request } from 'express'; +import camelcaseKeys from 'camelcase-keys'; +import { Route } from '@tupaia/server-boilerplate'; +import { DatatrakWebLeaderboardRequest } from '@tupaia/types'; + +export type LeaderboardRequest = Request< + DatatrakWebLeaderboardRequest.Params, + DatatrakWebLeaderboardRequest.ResBody, + DatatrakWebLeaderboardRequest.ReqBody, + DatatrakWebLeaderboardRequest.ReqQuery +>; + +export class LeaderboardRoute extends Route { + public async buildResponse() { + const { models } = this.req; + + const leaderboard = await models.surveyResponse.getLeaderboard(); + return camelcaseKeys(leaderboard, { deep: true }); + } +} diff --git a/packages/datatrak-web-server/src/routes/index.ts b/packages/datatrak-web-server/src/routes/index.ts index e283e14623..65ac7d95f3 100644 --- a/packages/datatrak-web-server/src/routes/index.ts +++ b/packages/datatrak-web-server/src/routes/index.ts @@ -11,3 +11,4 @@ export { ProjectsRequest, ProjectsRoute } from './ProjectsRoute'; export { EntitiesRequest, EntitiesRoute } from './EntitiesRoute'; export { ProjectRequest, ProjectRoute } from './ProjectRoute'; export { SubmitSurveyRequest, SubmitSurveyRoute } from './SubmitSurvey/SubmitSurveyRoute'; +export { LeaderboardRequest, LeaderboardRoute } from './LeaderboardRoute'; diff --git a/packages/datatrak-web-server/src/types.ts b/packages/datatrak-web-server/src/types.ts index 03e95708ce..cd118e7e53 100644 --- a/packages/datatrak-web-server/src/types.ts +++ b/packages/datatrak-web-server/src/types.ts @@ -6,9 +6,11 @@ import { ModelRegistry, EntityModel, EntityType as BaseEntityType } from '@tupaia/database'; import { Model } from '@tupaia/server-boilerplate'; import { Entity } from '@tupaia/types'; +import { SurveyResponseModel } from './models'; export type EntityType = BaseEntityType & Entity; export interface DatatrakWebServerModelRegistry extends ModelRegistry { readonly entity: Model; + readonly surveyResponse: SurveyResponseModel; } diff --git a/packages/datatrak-web/src/api/queries/useLeaderboard.ts b/packages/datatrak-web/src/api/queries/useLeaderboard.ts index e3c6b6df33..3f48a1114f 100644 --- a/packages/datatrak-web/src/api/queries/useLeaderboard.ts +++ b/packages/datatrak-web/src/api/queries/useLeaderboard.ts @@ -3,9 +3,12 @@ * Copyright (c) 2017 - 2023 Beyond Essential Systems Pty Ltd */ import { useQuery } from 'react-query'; -import { LeaderboardItem } from '@tupaia/types'; +import { DatatrakWebLeaderboardRequest } from '@tupaia/types'; import { get } from '../api'; export const useLeaderboard = () => { - return useQuery(['leaderboard'], (): Promise => get('leaderboard')); + return useQuery( + ['leaderboard'], + (): Promise => get('leaderboard'), + ); }; diff --git a/packages/datatrak-web/src/features/Leaderboard/LeaderboardTable.tsx b/packages/datatrak-web/src/features/Leaderboard/LeaderboardTable.tsx index 64f5f93f78..4c5590cdc8 100644 --- a/packages/datatrak-web/src/features/Leaderboard/LeaderboardTable.tsx +++ b/packages/datatrak-web/src/features/Leaderboard/LeaderboardTable.tsx @@ -80,7 +80,7 @@ export const LeaderboardTable = ({ userRewards }: LeaderboardTableProps) => { if (isLoading) return null; const userIsInLeaderboard = - user && leaderboard?.some(({ user_id: userId }) => userId === user.id); + user && leaderboard?.some(({ userId }) => userId === user.id); return ( @@ -93,7 +93,7 @@ export const LeaderboardTable = ({ userRewards }: LeaderboardTableProps) => { {leaderboard?.map( - ({ user_id: userId, first_name: firstName, last_name: lastName, coconuts }, i) => { + ({ userId, firstName, lastName, coconuts }, i) => { const isActiveUser = user && user.id === userId; return ( diff --git a/packages/meditrak-app-server/src/__tests__/__integration__/socialFeed/SocialFeedRoute.test.ts b/packages/meditrak-app-server/src/__tests__/__integration__/socialFeed/SocialFeedRoute.test.ts index edf9c932dd..4a2c76e05f 100644 --- a/packages/meditrak-app-server/src/__tests__/__integration__/socialFeed/SocialFeedRoute.test.ts +++ b/packages/meditrak-app-server/src/__tests__/__integration__/socialFeed/SocialFeedRoute.test.ts @@ -5,7 +5,6 @@ // eslint-disable-next-line import/no-extraneous-dependencies import MockDate from 'mockdate'; - import { constructAccessToken } from '@tupaia/auth'; import { findOrCreateDummyRecord, @@ -60,6 +59,7 @@ describe('socialFeed', () => { }); const databaseTimezone = await models.database.getTimezone(); + const timezoneConvertedFeedItems = FEED_ITEMS.map(feedItem => ({ ...feedItem, creation_date: new Date(feedItem.creation_date).toLocaleString(databaseTimezone), // Adjust for timezone so tests work regardless of db timezone @@ -69,6 +69,7 @@ describe('socialFeed', () => { timezoneConvertedFeedItems, countryCodeToId, ); + await Promise.all( feedItemsToInsert.map(async feedItem => { await findOrCreateDummyRecord(models.feedItem, feedItem, {}); @@ -113,6 +114,7 @@ describe('socialFeed', () => { expect(hasMorePages).toBe(false); expect(pageNumber).toBe(0); const expectedItems = replaceItemsCountryWithCountryId(FEED_ITEMS, countryCodeToId); + addLeaderboardAsThirdItem(expectedItems, LEADERBOARD_ITEM); expect(filterItemFields(items)).toEqual(expectedItems); }); diff --git a/packages/meditrak-app-server/src/routes/SocialFeedRoute.ts b/packages/meditrak-app-server/src/routes/SocialFeedRoute.ts index 3564d216f7..478574700a 100644 --- a/packages/meditrak-app-server/src/routes/SocialFeedRoute.ts +++ b/packages/meditrak-app-server/src/routes/SocialFeedRoute.ts @@ -35,8 +35,8 @@ export type SocialFeedRequest = Request< export class SocialFeedRoute extends Route { private async getLeaderboardFeedItem() { - const { ctx } = this.req; - const leaderboard = await ctx.services.central.fetchResources('/leaderboard'); + const { models } = this.req; + const leaderboard = await models.surveyResponse.getLeaderboard(); return { id: 'leaderboard', diff --git a/packages/types/src/types/requests/datatrak-web-server/LeaderboardRequest.ts b/packages/types/src/types/requests/datatrak-web-server/LeaderboardRequest.ts new file mode 100644 index 0000000000..9bd231b478 --- /dev/null +++ b/packages/types/src/types/requests/datatrak-web-server/LeaderboardRequest.ts @@ -0,0 +1,12 @@ +/* + * Tupaia + * Copyright (c) 2017 - 2023 Beyond Essential Systems Pty Ltd + */ + +import { LeaderboardItem } from '../../models-extra'; + +export type Params = Record; + +export type ResBody = LeaderboardItem[]; +export type ReqBody = Record; +export type ReqQuery = Record; diff --git a/packages/types/src/types/requests/datatrak-web-server/index.ts b/packages/types/src/types/requests/datatrak-web-server/index.ts index 02b2d29825..9c924ddc4e 100644 --- a/packages/types/src/types/requests/datatrak-web-server/index.ts +++ b/packages/types/src/types/requests/datatrak-web-server/index.ts @@ -10,3 +10,4 @@ export * as DatatrakWebProjectsRequest from './ProjectsRequest'; export * as DatatrakWebSurveyRequest from './SurveyRequest'; export * as DatatrakWebSubmitSurveyRequest from './SubmitSurveyRequest'; export * as DatatrakWebSurveyResponsesRequest from './SurveyResponsesRequest'; +export * as DatatrakWebLeaderboardRequest from './LeaderboardRequest'; diff --git a/packages/types/src/types/requests/index.ts b/packages/types/src/types/requests/index.ts index 86b1f9b442..c275dd253b 100644 --- a/packages/types/src/types/requests/index.ts +++ b/packages/types/src/types/requests/index.ts @@ -14,6 +14,7 @@ export { DatatrakWebSubmitSurveyRequest, DatatrakWebSurveyRequest, DatatrakWebSurveyResponsesRequest, + DatatrakWebLeaderboardRequest, } from './datatrak-web-server'; export { TupaiaWebChangePasswordRequest,