Skip to content

Commit c6dff29

Browse files
committed
feat(ue): add insue from dfp pdfs
1 parent 691a301 commit c6dff29

File tree

9 files changed

+2642
-1492
lines changed

9 files changed

+2642
-1492
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@
5656
"multer": "1.4.5-lts.1",
5757
"pactum-matchers": "^1.1.7",
5858
"passport-jwt": "^4.0.1",
59+
"pdfjs-dist": "^5.0.375",
5960
"pdfkit": "^0.14.0",
6061
"prisma": "^5.17.0",
6162
"reflect-metadata": "^0.1.14",

pnpm-lock.yaml

Lines changed: 2541 additions & 1480 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
@@ -263,8 +263,9 @@ model UeAlias {
263263
}
264264

265265
model Ueof {
266-
code String @id @db.VarChar(20)
267-
siepId Int @unique
266+
code String @id @db.VarChar(20)
267+
siepId Int @unique
268+
inscriptionCode String?
268269
269270
available Boolean @default(false)
270271
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');
@@ -329,12 +385,36 @@ async function main() {
329385
}),
330386
),
331387
);
332-
console.info('\x1b[42;30m✅ Import complete\x1b[0m');
333388
} catch (error) {
334389
console.error(
335390
'\x1b[41;30mAn error occurred while importing UE requirements. Try `$ pnpm seed:ue:aliases` first.\x1b[0m',
336391
);
392+
return;
393+
}
394+
console.info('\x1b[42;30mFetching current INSUEs\x1b[0m');
395+
try {
396+
const insues = await fetchINSUE();
397+
await Promise.all(
398+
insues.map(([ueof, insue]) =>
399+
prisma.ueof.updateMany({
400+
where: {
401+
code: {
402+
startsWith: ueof.slice(4),
403+
},
404+
},
405+
data: {
406+
inscriptionCode: insue,
407+
},
408+
}),
409+
),
410+
);
411+
} catch (error) {
412+
console.error(
413+
'\x1b[41;30mAn error occurred while importing INSUEs. Check the network connection or DFP pdf structure.\x1b[0m',
414+
);
415+
return;
337416
}
417+
console.info('\x1b[42;30m✅ Import complete\x1b[0m');
338418
}
339419

340420
main();

src/std.type.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 (in place) and returns it.
99
* Array is sorted based on a mapper function, that returns in order the values by which to sort the array.
@@ -44,7 +44,7 @@ declare global {
4444
}
4545
}
4646

47-
Array.prototype.groupyBy = function <T, K extends string | number | symbol>(
47+
Array.prototype.groupBy = function <T, K extends string | number | symbol>(
4848
this: Array<T>,
4949
keyMapper: (entity: T) => K,
5050
) {

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
@@ -191,6 +191,8 @@ export class UeController {
191191
ueofs: ue.ueofs.map((ueof) => ({
192192
code: ueof.code,
193193
name: ueof.name,
194+
siepId: ueof.siepId,
195+
inscriptionCode: ueof.inscriptionCode,
194196
credits: ueof.credits.map((c) => ({
195197
credits: c.credits,
196198
category: {
@@ -266,7 +268,7 @@ export class UeController {
266268
return Math.round((ponderation / coefficients) * 10) / 10;
267269
}
268270
// Ponderate the rates of each ueof
269-
const ueofRates = Object.entries(rates.groupyBy((rate) => rate.ueofCode)).map(
271+
const ueofRates = Object.entries(rates.groupBy((rate) => rate.ueofCode)).map(
270272
([ueofCode, rates]) =>
271273
[
272274
ueofCode,

test/declarations.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,8 @@ Spec.prototype.expectUe = function (
104104
updateYear: 2000 + Number(ue.ueofs[0].code.match(/\d+$/)?.[0] ?? 23),
105105
ueofs: ue.ueofs.map((ueof) => ({
106106
name: getTranslation(ueof.name, this.language),
107+
siepId: ueof.siepId,
108+
inscriptionCode: ueof.inscriptionCode,
107109
code: ueof.code,
108110
credits: ueof.credits.map((credit) => ({
109111
...omit(credit, 'id', 'ueofCode', 'categoryId', 'branchOptions'),

0 commit comments

Comments
 (0)