287 lines
10 KiB
JavaScript
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;
|
|
}
|
|
}
|