Skip to content

Commit db872ee

Browse files
committed
Implement getting quiz results on a per-member basis
1 parent 33df1e6 commit db872ee

File tree

12 files changed

+138
-16
lines changed

12 files changed

+138
-16
lines changed

src/dependencies.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,9 @@ export type Dependencies = {
4646
sheetId: string,
4747
from: O.Option<Date>
4848
) => TE.TaskEither<string, SheetDataTable['rows']>;
49+
getSheetDataByMemberNumber: (
50+
memberNumber: number,
51+
) => TE.TaskEither<string, SheetDataTable['rows']>,
4952
getTroubleTicketData: (
5053
from: O.Option<Date>
5154
) => TE.TaskEither<string, O.Option<TroubleTicketDataTable['rows']>>;

src/init-dependencies/init-dependencies.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import {Client} from '@libsql/client';
1212

1313
import {initSharedReadModel} from '../read-models/shared-state';
1414
import {lastSync} from '../sync-worker/db/last_sync';
15-
import {getSheetData} from '../sync-worker/db/get_sheet_data';
15+
import {getSheetData, getSheetDataByMemberNumber} from '../sync-worker/db/get_sheet_data';
1616
import {getTroubleTicketData} from '../sync-worker/db/get_trouble_ticket_data';
1717

1818
export const initLogger = (conf: Config) => {
@@ -79,6 +79,7 @@ export const initDependencies = (
7979
logger,
8080
lastQuizSync: lastSync(googleDB),
8181
getSheetData: getSheetData(googleDB),
82+
getSheetDataByMemberNumber: getSheetDataByMemberNumber(googleDB),
8283
getTroubleTicketData: getTroubleTicketData(
8384
googleDB,
8485
O.fromNullable(conf.TROUBLE_TICKET_SHEET)

src/read-models/external-state/equipment-quiz.ts

Lines changed: 64 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
11
import * as O from 'fp-ts/Option';
22
import * as TE from 'fp-ts/TaskEither';
33
import * as RA from 'fp-ts/ReadonlyArray';
4+
import * as RR from 'fp-ts/ReadonlyRecord';
45

56
import {Dependencies} from '../../dependencies';
67
import {SheetDataTable} from '../../sync-worker/google/sheet-data-table';
78
import {pipe} from 'fp-ts/lib/function';
89
import {Equipment, MemberCoreInfo} from '../shared-state/return-types';
910
import {DateTime, Duration} from 'luxon';
1011
import { ReadonlyRecord } from 'fp-ts/lib/ReadonlyRecord';
11-
import { UUID } from 'io-ts-types';
12+
import { EquipmentId } from '../../types/equipment-id';
1213

1314
export type EquipmentQuizResults = {
1415
passedQuizes: SheetDataTable['rows'];
@@ -29,20 +30,23 @@ export type MemberAwaitingTraining = Pick<
2930
waitingSince: Date;
3031
};
3132

33+
const isPassed = (row: SheetDataTable['rows'][0]) => row.percentage >= 100;
34+
const isFailed = (row: SheetDataTable['rows'][0]) => !isPassed(row);
35+
3236
const extractPassedQuizes = (
3337
sheetData: SheetDataTable['rows']
3438
): SheetDataTable['rows'] =>
3539
pipe(
3640
sheetData,
37-
RA.filter(row => row.percentage === 100)
41+
RA.filter(isPassed)
3842
);
3943

4044
const extractFailedQuizes = (
4145
sheetData: SheetDataTable['rows']
4246
): SheetDataTable['rows'] =>
4347
pipe(
4448
sheetData,
45-
RA.filter(row => row.percentage < 100)
49+
RA.filter(isFailed)
4650
);
4751

4852
const getQuizResults = (
@@ -128,20 +132,69 @@ export const getFullQuizResultsForEquipment = (
128132
);
129133

130134
export type FullQuizResultsForMember = {
131-
equipmentQuizPassedAt: ReadonlyRecord<UUID, Date>,
132-
equipmentQuizAttempted: ReadonlyRecord<UUID, {
135+
equipmentQuizPassedAt: ReadonlyRecord<EquipmentId, ReadonlyArray<Date>>,
136+
equipmentQuizAttempted: ReadonlyRecord<EquipmentId, ReadonlyArray<{
137+
response_submitted: Date,
138+
sheet_id: string;
139+
score: number;
140+
max_score: number;
141+
percentage: number;
142+
}>>,
143+
orphanedQuizAttempts: ReadonlyArray<{
133144
response_submitted: Date,
134145
sheet_id: string;
135146
score: number;
136147
max_score: number;
137148
percentage: number;
138-
}>
149+
}>,
139150
};
140151

141152
export const getFullQuizResultsForMember = (
142-
deps: Pick<Dependencies, 'sharedReadModel' | 'getSheetData'>,
153+
deps: Pick<Dependencies, 'sharedReadModel' | 'getSheetDataByMemberNumber'>,
143154
memberNumber: number
144-
): TE.TaskEither<string, FullQuizResultsForMember> => {
145-
146-
147-
};
155+
): TE.TaskEither<string, FullQuizResultsForMember> => pipe(
156+
deps.getSheetDataByMemberNumber(memberNumber),
157+
TE.map(
158+
qr => {
159+
const equipmentQuizPassedAt: Record<EquipmentId, Date[]> = {};
160+
const equipmentQuizAttempted: Record<EquipmentId, {
161+
response_submitted: Date,
162+
sheet_id: string;
163+
score: number;
164+
max_score: number;
165+
percentage: number;
166+
}[]> = {};
167+
const orphanedQuizAttempts: {
168+
response_submitted: Date,
169+
sheet_id: string;
170+
score: number;
171+
max_score: number;
172+
percentage: number;
173+
}[] = [];
174+
const trainingSheetMapping = deps.sharedReadModel.equipment.getTrainingSheetIdMapping();
175+
for (const row of qr) {
176+
const equipmentId = RR.lookup(row.sheet_id)(trainingSheetMapping);
177+
if (O.isNone(equipmentId)) {
178+
orphanedQuizAttempts.push(row);
179+
} else {
180+
if (isPassed(row)) {
181+
if (!equipmentQuizPassedAt[equipmentId.value]) {
182+
equipmentQuizPassedAt[equipmentId.value] = [];
183+
}
184+
equipmentQuizPassedAt[equipmentId.value].push(row.response_submitted);
185+
} else {
186+
if (!equipmentQuizAttempted[equipmentId.value]) {
187+
equipmentQuizAttempted[equipmentId.value] = [];
188+
}
189+
equipmentQuizAttempted[equipmentId.value].push(row);
190+
}
191+
}
192+
}
193+
return {
194+
equipmentQuizPassedAt,
195+
equipmentQuizAttempted,
196+
orphanedQuizAttempts,
197+
}
198+
}
199+
)
200+
);

src/read-models/shared-state/equipment/get.ts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
11
import {pipe} from 'fp-ts/lib/function';
22
import {BetterSQLite3Database} from 'drizzle-orm/better-sqlite3';
3-
import {eq} from 'drizzle-orm';
3+
import {eq, isNotNull} from 'drizzle-orm';
44
import * as O from 'fp-ts/Option';
55
import * as RA from 'fp-ts/ReadonlyArray';
6+
import * as RR from 'fp-ts/ReadonlyRecord';
67
import {equipmentTable} from '../state';
78
import {MinimalEquipment} from '../return-types';
89
import {UUID} from 'io-ts-types';
10+
import { ReadonlyRecord } from 'fp-ts/lib/ReadonlyRecord';
11+
import { TrainingSheetId } from '../../../types/training-sheet';
12+
import { EquipmentId } from '../../../types/equipment-id';
913

1014
const transformRow = <
1115
R extends {
@@ -47,3 +51,18 @@ export const getAllEquipmentMinimal = (
4751
db: BetterSQLite3Database
4852
): ReadonlyArray<MinimalEquipment> =>
4953
pipe(db.select().from(equipmentTable).all(), RA.map(transformRow));
54+
55+
export const getTrainingSheetIdMapping = (
56+
db: BetterSQLite3Database
57+
) => (): ReadonlyRecord<TrainingSheetId, EquipmentId> =>
58+
pipe(
59+
db.select({
60+
trainingSheetId: equipmentTable.trainingSheetId,
61+
id: equipmentTable.id,
62+
}).from(equipmentTable).where(isNotNull(equipmentTable.trainingSheetId)).all(),
63+
RA.map(
64+
row => ([row.trainingSheetId!, row.id as UUID])
65+
),
66+
(x: ReadonlyArray<[string, UUID]>) => x,
67+
RR.fromEntries
68+
)

src/read-models/shared-state/index.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,10 @@ import {dumpCurrentState, SharedDatabaseDump} from './debug/dump';
2727
import {MemberLinking} from './member-linking';
2828
import {DateTime} from 'luxon';
2929
import {getLastSent} from './training-stat-notifications/get-last-sent';
30+
import { ReadonlyRecord } from 'fp-ts/lib/ReadonlyRecord';
31+
import { TrainingSheetId } from '../../types/training-sheet';
32+
import { EquipmentId } from '../../types/equipment-id';
33+
import { getTrainingSheetIdMapping } from './equipment/get';
3034

3135
export type SharedReadModel = {
3236
db: BetterSQLite3Database;
@@ -44,6 +48,7 @@ export type SharedReadModel = {
4448
equipment: {
4549
get: (id: UUID) => O.Option<Equipment>;
4650
getAll: () => ReadonlyArray<Equipment>;
51+
getTrainingSheetIdMapping: () => ReadonlyRecord<TrainingSheetId, EquipmentId>;
4752
};
4853
area: {
4954
get: (id: UUID) => O.Option<Area>;
@@ -94,6 +99,7 @@ export const initSharedReadModel = (
9499
equipment: {
95100
get: getEquipmentFull(readModelDb, linking),
96101
getAll: getAllEquipmentFull(readModelDb, linking),
102+
getTrainingSheetIdMapping: getTrainingSheetIdMapping(readModelDb),
97103
},
98104
area: {
99105
get: getAreaFull(readModelDb, linking),

src/read-models/shared-state/member-linking.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
type MemberNumber = number;
1+
import { MemberNumber } from "../../types/member-number";
22

33
export class MemberLinking {
44
// Stores the linking between member numbers.

src/sync-worker/db/get_sheet_data.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,3 +49,33 @@ export const getSheetData =
4949
),
5050
TE.map(data => data.rows)
5151
);
52+
53+
export const getSheetDataByMemberNumber =
54+
(googleDB: Client) =>
55+
(
56+
memberNumber: number,
57+
): TE.TaskEither<string, SheetDataTable['rows']> =>
58+
pipe(
59+
TE.tryCatch<string, ResultSet>(
60+
() => googleDB.execute(
61+
` SELECT *
62+
FROM sheet_data
63+
WHERE member_number_provided = ?
64+
`, [memberNumber]
65+
),
66+
reason =>
67+
`Failed to get sheet data for memberNumber '${memberNumber}': ${(reason as Error).message}`
68+
),
69+
TE.flatMapEither<ResultSet, string, SheetDataTable>(data =>
70+
pipe(
71+
data,
72+
SheetDataTable.decode,
73+
E.mapLeft(
74+
e =>
75+
'Failed to pull sheet data due to malformed data: ' +
76+
formatValidationErrors(e).join(',')
77+
)
78+
)
79+
),
80+
TE.map(data => data.rows)
81+
);

src/types/equipment-id.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
import { UUID } from "io-ts-types";
2+
3+
// Simple type alias to make typing easier to follow.
4+
export type EquipmentId = UUID;

src/types/member-number.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
// Simple type alias to make typing easier to follow.
2+
export type MemberNumber = number;

src/types/training-sheet.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
// Simple type alias to make typing easier to follow.
2+
export type TrainingSheetId = string;

0 commit comments

Comments
 (0)