Files
Timetable-V2/server/parser/index.js

287 lines
10 KiB
JavaScript

import Prisma from "@prisma/client";
import { log, getLogPath } from "../logs.js";
const prisma = new Prisma.PrismaClient();
export class Parser {
constructor(fileProvider, documentParser, interval) {
this.fileProvider = fileProvider;
this.documentParser = documentParser;
// Schedule plan updates
setInterval(() => this.updatePlan(), interval);
// Do the first update instantly
this.updatePlan();
}
async updatePlan() {
const startedAt = new Date();
try {
// Request substitution plan files using the fileProvider
const files = await this.fileProvider.getFiles();
const plans = [];
// Parse them using the provided parser
for (const file of files) {
// Parse the substitution plan
const parsed = this.documentParser(file);
plans.push(parsed);
}
// Create a new parse event
const parseEvent = await prisma.parseEvent.create({
data: {
logFile: getLogPath(),
originalData: "",
duration: new Date() - startedAt,
succeeded: true,
},
});
// Group plans by date to prevent having
// multiple plan files with the same date
const dayPlans = [];
for (const plan of plans) {
const foundPlan = dayPlans.find((e) => e.date == plan.date);
if (!foundPlan) {
// Make sure to not insert duplicate substitutions within a file
const changes = structuredClone(plan.changes);
const cleanedChanges = [];
for (const change of changes) {
const changeExists = cleanedChanges.find(
(e) => JSON.stringify(e) == JSON.stringify(change),
);
if (!changeExists) {
cleanedChanges.push(change);
}
// Use the new array of changes
plan.changes = cleanedChanges;
}
dayPlans.push(plan);
} else {
for (const change of plan.changes) {
// Make sure to not insert a substitution that already exists in the changes
const changeExists = foundPlan.changes.find(
(e) => JSON.stringify(e) == JSON.stringify(change),
);
if (!changeExists) {
foundPlan.changes.push(change);
}
}
}
}
// Insert substitutions of all substitution plans
for (const plan of dayPlans) {
await this.insertSubstitutions(plan, parseEvent);
}
} catch (error) {
// If something went wrong, create a failed
// parse event with the error message
await prisma.parseEvent.create({
data: {
logFile: getLogPath(),
originalData: error.toString(),
duration: new Date() - startedAt,
succeeded: false,
},
});
// Log the error
log("Parser / Main", "Parse event failed: " + error);
}
}
async insertSubstitutions(parsedData, parseEvent) {
const { date, changes } = parsedData;
const classList = await prisma.class.findMany();
const knownSubstitutions = await prisma.substitution.findMany({
where: {
date: new Date(new Date(date).setUTCHours(0, 0, 0, 0)),
removed: false,
},
});
// Loop through every change of the substitution plan
for (const change of changes) {
// Find all classes the substitution belongs to
const classes = this.getSubstitutionClasses(classList, change.class);
// If the substitution does not belong to any classes known
// by the server, use the provied class string instead
if (classes.length == 0) classes.push(change.class || "unknown");
// Workaround: no correct match possible for subsitutions of this
// type beacuse they do not have a class and a subject attribute
if (change.type == "Sondereins." && !change.subject) {
change.subject = change.notes;
}
if (change.type == "Sondereins." && !change.teacher) {
change.teacher = change.changedTeacher;
change.changedTeacher = "";
}
// Check if a substitution exists in the database that
// it similar enough to the entry in the substitution
// plan to be considered the same substitution
// (Date, Type, Lesson, Classes and Subject need to be the same)
const matchingSubstitutionId = knownSubstitutions.findIndex(
(substitution) => {
return (
substitution.date.getTime() ==
new Date(date).setUTCHours(0, 0, 0, 0) &&
substitution.rawType == change.type &&
substitution.lesson == change.lesson &&
classes.sort().join(",") == substitution.class.sort().join(",") &&
substitution.changedSubject == change.subject &&
substitution.teacher == (change.teacher || "")
);
},
);
const matchingSubstitution = knownSubstitutions[matchingSubstitutionId];
if (!matchingSubstitution) {
// If the substitution is new, create it in the database
const newSubstitution = await prisma.substitution.create({
data: {
class: classes,
date: new Date(date),
type:
change.type == "Entfall" ||
change.type == "eigenverantwortliches Arbeiten"
? "cancellation"
: "change",
rawType: change.type,
lesson: parseInt(change.lesson),
teacher: change.teacher || "",
changedTeacher: change.changedTeacher,
changedRoom: change.room || undefined,
changedSubject: change.subject,
notes: change.notes,
removed: false,
},
});
// Also create a change entry for it
const substitutionChange = await prisma.substitutionChange.create({
data: {
substitutionId: newSubstitution.id,
type: "addition",
changes: {
class: classes,
type: change.type == "Entfall" ? "cancellation" : "change",
rawType: change.type,
lesson: parseInt(change.lesson),
date: new Date(date),
notes: change.notes,
teacher: change.teacher || "",
change: {
teacher: change.changedTeacher,
room: change.room || undefined,
subject: change.subject,
},
},
parseEventId: parseEvent.id,
},
});
log(
"Insert / DB",
`Created new substitution: S:${newSubstitution.id} C:${substitutionChange.id}`,
);
} else {
// If the entry was updated, find the differences
const differences = this.findDifferences(matchingSubstitution, change);
if (Object.keys(differences).length > 0) {
// If differences were found, update the entry in the database
const prismaOptions = {
where: {
id: matchingSubstitution.id,
},
data: {},
};
if (differences.teacher)
prismaOptions.data.changedTeacher = change.changedTeacher;
if (differences.room) prismaOptions.data.changedRoom = change.room;
if (differences.notes) prismaOptions.data.notes = change.notes;
await prisma.substitution.update(prismaOptions);
// And create a change event for it
const substitutionChange = await prisma.substitutionChange.create({
data: {
substitutionId: matchingSubstitution.id,
type: "change",
changes: differences,
parseEventId: parseEvent.id,
},
});
log(
"Insert / DB",
`Found changed substitution: S:${matchingSubstitution.id} C:${substitutionChange.id}`,
);
}
// Remove the substitution from the array to later know the
// entries that are not present in the substitution plan
knownSubstitutions.splice(matchingSubstitutionId, 1);
}
}
// Mark all entries as removed that were
// not found in the substitution plan
for (const remainingSubstitution of knownSubstitutions) {
await prisma.substitution.update({
where: {
id: remainingSubstitution.id,
},
data: {
removed: true,
},
});
const substitutionChange = await prisma.substitutionChange.create({
data: {
substitutionId: remainingSubstitution.id,
type: "deletion",
changes: {
class: remainingSubstitution.class,
type: remainingSubstitution.type,
rawType: remainingSubstitution.rawType,
lesson: remainingSubstitution.lesson,
date: remainingSubstitution.date.getTime(),
notes: remainingSubstitution.notes,
teacher: remainingSubstitution.teacher,
change: {
teacher: remainingSubstitution.changedTeacher,
room: remainingSubstitution.changedRoom,
subject: remainingSubstitution.changedSubject,
},
},
parseEventId: parseEvent.id,
},
});
log(
"Insert / DB",
`Deleted removed substitution: S:${remainingSubstitution.id} C:${substitutionChange.id}`,
);
}
}
getSubstitutionClasses(classList, classString) {
const matchingClasses = classList.filter((element) => {
const regex = new RegExp(element.regex);
return (classString || "").toLowerCase().match(regex);
});
return matchingClasses.map((e) => e.name);
}
findDifferences(currentSubstitution, newChange) {
const differences = {};
if (newChange.changedTeacher != currentSubstitution.changedTeacher) {
differences.teacher = {
before: currentSubstitution.changedTeacher,
after: newChange.changedTeacher,
};
}
if (newChange.room != currentSubstitution.changedRoom) {
differences.room = {
before: currentSubstitution.changedRoom,
after: newChange.room,
};
}
if (newChange.notes != currentSubstitution.notes) {
differences.notes = {
before: currentSubstitution.notes,
after: newChange.notes,
};
}
return differences;
}
}