Skip to content

Commit 2bf8663

Browse files
committed
feat(ue): add insue from dfp pdfs
1 parent 9986481 commit 2bf8663

File tree

9 files changed

+220
-12
lines changed

9 files changed

+220
-12
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@
5454
"multer": "1.4.5-lts.1",
5555
"pactum-matchers": "^1.1.7",
5656
"passport-jwt": "^4.0.1",
57+
"pdfjs-dist": "^5.0.375",
5758
"pdfkit": "^0.14.0",
5859
"prisma": "^5.17.0",
5960
"reflect-metadata": "^0.1.14",

pnpm-lock.yaml

Lines changed: 119 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

prisma/schema.prisma

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -227,8 +227,9 @@ model UeAlias {
227227
}
228228

229229
model Ueof {
230-
code String @id @db.VarChar(20)
231-
siepId Int @unique
230+
code String @id @db.VarChar(20)
231+
siepId Int @unique
232+
inscriptionCode String?
232233
233234
available Boolean @default(false)
234235
createdAt DateTime @default(now())

scripts/seed/ue.ts

Lines changed: 87 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { createReadStream } from 'fs';
22
import { createInterface } from 'readline/promises';
33
import { parse } from '@fast-csv/parse';
44
import { PrismaClient } from '@prisma/client';
5-
import '../../src/array';
5+
import type { TextItem } from 'pdfjs-dist/types/src/display/api';
66

77
const prisma = new PrismaClient();
88

@@ -51,6 +51,29 @@ async function findPadding(document: string) {
5151
});
5252
}
5353

54+
function sanitize<T>(obj: T): T {
55+
if (typeof obj === 'string') {
56+
const output = obj
57+
.replaceAll(/(?![\n\r])\s/g, ' ')
58+
.replaceAll(/½(?=uvre)/g, 'œ')
59+
.replaceAll(/(?<=c)½(?=ur)/g, 'œ')
60+
.replaceAll(/(?:Þ|(?<=\n|\r|^)[?\-\u00ad])\s?/g, '• ')
61+
.replaceAll(/\u00ad|/g, '-')
62+
.replaceAll(/a`/g, 'à')
63+
.replaceAll(/é´/g, 'é')
64+
.replaceAll('’', "'")
65+
.replaceAll(/(?<=[dls])¿/g, "'")
66+
.trim() as T & string;
67+
const match = output.match(/[^a-zA-Z0-9 éèàâ'ôîïêù\-\n\r?(),;":/_.ûÉÊœ«»ç&+]/g);
68+
if (match?.length)
69+
console.warn(`\x1b[45;30m[UNEXPECTED_GLYPH] ${match.map((c) => `"${c}"`).join(', ')}\x1b[0m. Check "${output}"`);
70+
return output;
71+
} else if (obj instanceof Array) return obj.map(sanitize) as T;
72+
else if (typeof obj === 'object')
73+
return Object.fromEntries(Object.entries(obj).map(([k, v]) => [k, sanitize(v)])) as T;
74+
return obj;
75+
}
76+
5477
async function parseDocument(document: string) {
5578
return new Promise<UEOF[]>(async (resolve, reject) => {
5679
const ueofs: UEOF[] = [];
@@ -122,17 +145,50 @@ async function parseDocument(document: string) {
122145
resolve(
123146
// A single UEOF can appear multiple times in the CSV with a different requirement
124147
// We group the UEOFs by their code and merge the requirements
125-
Object.values(ueofs.groupyBy((ueof) => ueof.ueof_code)).map((duplicates) => ({
126-
...duplicates[0],
127-
requirements: duplicates.flatMap((ueof) => ueof.requirements),
128-
})),
148+
sanitize(
149+
Object.values(
150+
ueofs.reduce((acc, ueof) => {
151+
const key = ueof.ueof_code;
152+
if (!acc[key]) acc[key] = [];
153+
acc[key].push(ueof);
154+
return acc;
155+
}, {} as { [key: string]: UEOF[] }),
156+
).map((duplicates) => ({
157+
...duplicates[0],
158+
requirements: duplicates.flatMap((ueof) => ueof.requirements),
159+
})),
160+
),
129161
),
130162
);
131163
});
132164
}
133165

166+
async function fetchINSUE() {
167+
const pdfjs = (await new Function(
168+
"return import('pdfjs-dist/legacy/build/pdf.mjs')",
169+
)()) as typeof import('pdfjs-dist');
170+
const variants = ['TC', 'RT', 'ISI', 'SN', 'GI', 'GM', 'MTE', 'A2I', 'MASTER', 'EC', 'HUMANITE', 'MANAGEMENT'];
171+
const entries: [string, string][] = [];
172+
for (const variant of variants) {
173+
const doc = await pdfjs.getDocument(
174+
`https://gestion.utt.fr/applis/service2/insuv/sql/fichiers/EdT_previsionnel_${variant}.pdf`,
175+
).promise;
176+
for (let i = 0; i < doc.numPages; i++) {
177+
const page = await doc.getPage(i + 1);
178+
const textContent = await page.getTextContent();
179+
entries.push(
180+
...textContent.items
181+
.map((value: TextItem, i) => [value.str, i] as [string | null, number])
182+
.filter(([item]) => item?.match(/^[AP]\d{2}_[^_]+_[^_]+_[^_]+/))
183+
.map<[string, string]>(([item, i]) => [item, (<TextItem>textContent.items[i + 2]).str]), // + 2 because there is an extra space
184+
);
185+
}
186+
}
187+
return entries;
188+
}
189+
134190
async function main() {
135-
const importYear = new Date().getFullYear() - 1; // Can be changed to the year of the import if done with web interface
191+
const importYear = Number(process.argv[2]) || new Date().getFullYear(); // Can be changed to the year of the import if done with web interface
136192

137193
console.info('\x1b[42;30mFetching UE list\x1b[0m');
138194
const ues = await parseDocument('scripts/seed/dfp_data.csv');
@@ -333,12 +389,36 @@ async function main() {
333389
}),
334390
),
335391
);
336-
console.info('\x1b[42;30m✅ Import complete\x1b[0m');
337392
} catch (error) {
338393
console.error(
339394
'\x1b[41;30mAn error occurred while importing UE requirements. Try `$ pnpm seed:ue:aliases` first.\x1b[0m',
340395
);
396+
return;
397+
}
398+
console.info('\x1b[42;30mFetching current INSUEs\x1b[0m');
399+
try {
400+
const insues = await fetchINSUE();
401+
await Promise.all(
402+
insues.map(([ueof, insue]) =>
403+
prisma.ueof.updateMany({
404+
where: {
405+
code: {
406+
startsWith: ueof.slice(4),
407+
},
408+
},
409+
data: {
410+
inscriptionCode: insue,
411+
},
412+
}),
413+
),
414+
);
415+
} catch (error) {
416+
console.error(
417+
'\x1b[41;30mAn error occurred while importing INSUEs. Check the network connection or DFP pdf structure.\x1b[0m',
418+
);
419+
return;
341420
}
421+
console.info('\x1b[42;30m✅ Import complete\x1b[0m');
342422
}
343423

344424
main();

src/array.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ declare global {
33
/**
44
* Groups the current array by a key, using a mapper function.
55
*/
6-
groupyBy<K extends string | number | symbol>(keyMapper: (entity: T) => K): { [key in K]: T[] };
6+
groupBy<K extends string | number | symbol>(keyMapper: (entity: T) => K): { [key in K]: T[] };
77
/**
88
* Sorts the current array and returns it.
99
* Array is sorted based on a mapper function, that returns in order the values by which to sort the array.
@@ -29,7 +29,7 @@ declare global {
2929
}
3030
}
3131

32-
Array.prototype.groupyBy = function <T, K extends string | number | symbol>(
32+
Array.prototype.groupBy = function <T, K extends string | number | symbol>(
3333
this: Array<T>,
3434
keyMapper: (entity: T) => K,
3535
) {

src/ue/dto/res/ue-detail-res.dto.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ export class UeofDetailResDto {
1818
code: string;
1919
@ApiProperty({ type: String })
2020
name: Translation;
21+
siepId: number;
22+
inscriptionCode: string;
2123

2224
credits: UeDetailResDto_Credit[];
2325
info: UeDetailResDto_Info;

src/ue/interfaces/ue.interface.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ const UE_SELECT_FILTER = {
1212
name: translationSelect,
1313
available: true,
1414
siepId: true,
15+
inscriptionCode: true,
1516
requirements: {
1617
select: {
1718
code: true,

src/ue/ue.controller.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,8 @@ export class UeController {
190190
ueofs: ue.ueofs.map((ueof) => ({
191191
code: ueof.code,
192192
name: ueof.name,
193+
siepId: ueof.siepId,
194+
inscriptionCode: ueof.inscriptionCode,
193195
credits: ueof.credits.map((c) => ({
194196
credits: c.credits,
195197
category: {
@@ -265,7 +267,7 @@ export class UeController {
265267
return Math.round((ponderation / coefficients) * 10) / 10;
266268
}
267269
// Ponderate the rates of each ueof
268-
const ueofRates = Object.entries(rates.groupyBy((rate) => rate.ueofCode)).map(
270+
const ueofRates = Object.entries(rates.groupBy((rate) => rate.ueofCode)).map(
269271
([ueofCode, rates]) =>
270272
[
271273
ueofCode,

test/declarations.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,8 @@ Spec.prototype.expectUe = function (
8282
updateYear: 2000 + Number(ue.ueofs[0].code.match(/\d+$/)?.[0] ?? 23),
8383
ueofs: ue.ueofs.map((ueof) => ({
8484
name: getTranslation(ueof.name, this.language),
85+
siepId: ueof.siepId,
86+
inscriptionCode: ueof.inscriptionCode,
8587
code: ueof.code,
8688
credits: ueof.credits.map((credit) => ({
8789
...omit(credit, 'id', 'ueofCode', 'categoryId', 'branchOptions'),

0 commit comments

Comments
 (0)