Skip to content

Conversation

@Sana9058
Copy link

This PR adds a basic HTML editor page for admins.

Changes included:

  • Added an admin editor page with a POST form
  • Registered GET and POST routes for /editor
  • Linked the editor from the admin dashboard

This sets up the structure for editing templates via the admin UI.

@Schlaumeier5 Schlaumeier5 self-requested a review January 19, 2026 14:56
Copy link
Member

@Schlaumeier5 Schlaumeier5 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems nice so far, but I still need a few things

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add %[site;title=HTML Editor;content=!FOLLOWS] at the beginning?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also maybe add a text field where you can put in the file name it should be saved as.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Schlaumeier5 Thanks for the review! I’ve added the template header to editor.html and registered a stub POST handler for /editor in PostRequestHandler.java as suggested.
The POST handler currently redirects back to /editor, and can be extended later with the actual save logic.
Please let me know if you’d like any adjustments or a different approach.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Registering post handlers happens in Java code, see here:

public static void registerHandlers() {
HttpHandler.registerPostRequestHandler("/login", AccessLevel.PUBLIC, (rq) -> {
String username = prepare(rq.getString("username"), false);
// Do not sanitize / url-decode password to allow special characters like %
// This is safe as we calculate the hash value anyways
String password = rq.getString("password");
// Check login credentials in the database
if (Server.getInstance().isValidUser(username, password)) {
SessionManager manager = Server.getInstance().getWebServer().getSessionManager();
Session session = manager.getSession(rq);
manager.addSessionUser(session, username);
return PostResponse.ok("Login successful", ContentType.TEXT_PLAIN, rq, session.createSessionCookie());
} else {
return PostResponse.unauthorized("Wrong credentials!", rq);
}
});
HttpHandler.registerPostRequestHandler("/add-students", AccessLevel.ADMIN, (rq) ->
handleBatchInsertCSV(rq, "students", ContentType.CSV, t -> {
try {
return Student.generateStudentsFromCSV(t);
} catch (SQLException e) {
throw new IllegalStateException(e);
}
}, PostRequestHandler::csvResult)
);
HttpHandler.registerPostRequestHandler("/add-teachers", AccessLevel.ADMIN, (rq) ->
handleBatchInsertCSV(rq, "teachers", ContentType.CSV, t -> {
try {
return Teacher.generateTeachersFromCSV(t);
} catch (SQLException e) {
throw new IllegalStateException(e);
}
}, PostRequestHandler::csvResult)
);
HttpHandler.registerPostRequestHandler("/add-rooms", AccessLevel.ADMIN, (rq) ->
handleBatchInsertCSV(rq, "rooms", ContentType.JSON, t -> {
try {
return Room.generateRoomsFromCSV(t);
} catch (SQLException e) {
throw new IllegalStateException(e);
}
}, Arrays::toString)
);
HttpHandler.registerPostRequestHandler("/add-teacher", AccessLevel.ADMIN, (rq) -> {
String firstName = prepare(rq.getString("firstName"));
String lastName = prepare(rq.getString("lastName"));
String email = prepare(rq.getString("email"), false);
String password = Teacher.generateRandomPassword(12, (rq.getContentLength() << 4 + firstName.length() + lastName.length()) << 7 + System.currentTimeMillis() * new Random().nextInt());
Teacher teacher = Teacher.registerTeacher(firstName, lastName, email, password);
return PostResponse.ok(teacher.toString().replace("}", "") + ", \"password\": " + password + "}", ContentType.JSON, rq);
});
HttpHandler.registerPostRequestHandler("/add-subject", AccessLevel.ADMIN, (rq) -> {
Subject.addSubject(rq.getString("name"));
return PostResponse.redirect("/manage_subjects", rq);
});
HttpHandler.registerPostRequestHandler("/add-class", AccessLevel.ADMIN, (rq) -> {
SchoolClass.addClass(rq.getString("className"), rq.getInt("grade"));
return PostResponse.redirect("/manage_subjects", rq);
});
HttpHandler.registerPostRequestHandler("/lpt-file", AccessLevel.ADMIN, (rq) -> {
String file = prepare(rq.getBodyAsString().replaceFirst("file=", "").replace("Â", ""));
Application.getInstance().readFile(file);
return PostResponse.ok("File data stored", ContentType.TEXT_PLAIN, rq);
});
HttpHandler.registerPostRequestHandler("/subject-request", AccessLevel.USER, (rq) -> {
Student student = rq.getCurrentStudent();
Subject subject = rq.getSubject();
SubjectRequest subjectRequest = rq.getSubjectRequest();
if (student != null) {
if (rq.getBoolean("remove")) {
student.removeSubjectRequest(subject, subjectRequest);
return PostResponse.ok("Removed request", ContentType.TEXT_PLAIN, rq);
} else {
student.addSubjectRequest(subject, subjectRequest);
return PostResponse.ok("Added request", ContentType.TEXT_PLAIN, rq);
}
} else {
return PostResponse.unauthorized(rq);
}
});
HttpHandler.registerPostRequestHandler("/current-topic", AccessLevel.USER, (rq) -> {
Student student = rq.getCurrentStudent();
Subject subject = rq.getSubject();
if (student == null) return PostResponse.unauthorized(rq);
Topic topic = student.getCurrentTopic(subject);
if (topic == null) return PostResponse.badRequest("No current topic for this subject.", rq);
return PostResponse.ok(topic.toJSON(), ContentType.JSON, rq);
});
HttpHandler.registerPostRequestHandler("/change-current-topic", AccessLevel.TEACHER, (rq) -> {
Student student = rq.getCurrentStudent();
if (student == null) return PostResponse.unauthorized(rq);
Subject subject = rq.getSubject();
Topic topic = rq.getTopic();
if (subject == null || topic == null) return PostResponse.badRequest("Subject or topic id not found", rq);
student.setCurrentTopic(subject, topic);
return PostResponse.ok("Current topic changed successfully", ContentType.TEXT_PLAIN, rq);
});
HttpHandler.registerPostRequestHandler("/tasks", AccessLevel.USER, (rq) -> {
return PostResponse.ok(JSONUtils.toJSON(rq.getTaskList()), ContentType.JSON, rq);
});
HttpHandler.registerPostRequestHandler("/update-room", AccessLevel.USER, (rq) -> {
Student student = rq.getCurrentStudent();
if (student == null) return PostResponse.unauthorized(rq);
Room room = rq.getRoom();
if (room == null) return PostResponse.badRequest("Room not found", rq);
student.setCurrentRoom(room);
return PostResponse.ok("Changed current room", ContentType.TEXT_PLAIN, rq);
});
HttpHandler.registerPostRequestHandler("/begin-task", AccessLevel.USER, (rq) -> handleTaskChange(rq, Task.STATUS_IN_PROGRESS));
HttpHandler.registerPostRequestHandler("/complete-task", AccessLevel.USER, (rq) -> handleTaskChange(rq, Task.STATUS_COMPLETED));
HttpHandler.registerPostRequestHandler("/cancel-task", AccessLevel.USER, (rq) -> handleTaskChange(rq, Task.STATUS_NOT_STARTED));
HttpHandler.registerPostRequestHandler("/reopen-task", AccessLevel.USER, (rq) -> handleTaskChange(rq, Task.STATUS_NOT_STARTED));
HttpHandler.registerPostRequestHandler("/lock-task", AccessLevel.USER, (rq) -> handleTaskChange(rq, Task.STATUS_LOCKED));
HttpHandler.registerPostRequestHandler("/student-data", AccessLevel.TEACHER, PostRequestHandler::handleStudentGetData);
HttpHandler.registerPostRequestHandler("/rooms", AccessLevel.TEACHER, PostRequestHandler::handleStudentGetData);
HttpHandler.registerPostRequestHandler("/student-subjects", AccessLevel.TEACHER, PostRequestHandler::handleStudentGetData);
HttpHandler.registerPostRequestHandler("/teacher-classes", AccessLevel.ADMIN, PostRequestHandler::handleTeacherGetData);
HttpHandler.registerPostRequestHandler("/teacher-subjects", AccessLevel.ADMIN, PostRequestHandler::handleTeacherGetData);
HttpHandler.registerPostRequestHandler("/student-list", AccessLevel.TEACHER, (rq) -> {
SchoolClass schoolClass = rq.getSchoolClass();
if (schoolClass == null) return PostResponse.notFound("School class not found", rq);
if (rq.getUser().isTeacher() && !rq.getUser().asTeacher().getClassIds().contains(schoolClass.getId()))
return PostResponse.forbidden("You are not allowed to access this class's student list.", rq);
List<Student> students = schoolClass.getStudents();
return PostResponse.ok(
JSONUtils.toJSON(students, (student, builder) -> {
builder
.addProperty("id", student.getId())
.addProperty("name", student.getFirstName() + " " + student.getLastName())
.addProperty("actionRequired", student.isActionRequired())
.addProperty("graduationLevel", student.getGraduationLevel())
.addProperty("room", student.getCurrentRoom() != null ? student.getCurrentRoom().getLabel() : "None");
if (rq.getJson().containsKey("subjectId") && rq.getSubject() != null) {
Set<SubjectRequest> subjectRequests = student.getCurrentRequests(rq.getSubject());
builder.addProperty("experiment",subjectRequests.stream().anyMatch(r -> r == SubjectRequest.EXPERIMENT))
.addProperty("help", subjectRequests.stream().anyMatch(r -> r == SubjectRequest.HELP))
.addProperty("test", subjectRequests.stream().anyMatch(r -> r == SubjectRequest.EXAM))
.addProperty("partner", subjectRequests.stream().anyMatch(r -> r == SubjectRequest.PARTNER));
}
}),
ContentType.JSON, rq
);
});
HttpHandler.registerPostRequestHandler("/get-students-by-room", AccessLevel.TEACHER, (rq) -> {
Room room = rq.getRoom();
List<Student> students = Student.getByRoom(room);
return PostResponse.ok(
JSONUtils.toJSON(students, (student, builder) -> {
builder
.addProperty("id", student.getId())
.addProperty("name", student.getFirstName() + " " + student.getLastName())
.addProperty("actionRequired", student.isActionRequired())
.addProperty("graduationLevel", student.getGraduationLevel())
.addProperty("room", student.getCurrentRoom() != null ? student.getCurrentRoom().getLabel() : "None");
if (rq.getJson().containsKey("subjectId") && rq.getSubject() != null) {
Set<SubjectRequest> subjectRequests = student.getCurrentRequests(rq.getSubject());
builder.addProperty("experiment",subjectRequests.stream().anyMatch(r -> r == SubjectRequest.EXPERIMENT))
.addProperty("help", subjectRequests.stream().anyMatch(r -> r == SubjectRequest.HELP))
.addProperty("test", subjectRequests.stream().anyMatch(r -> r == SubjectRequest.EXAM))
.addProperty("partner", subjectRequests.stream().anyMatch(r -> r == SubjectRequest.PARTNER));
}
}),
ContentType.JSON, rq
);
});
HttpHandler.registerPostRequestHandler("/grade-list", AccessLevel.PUBLIC, (rq) -> {
return PostResponse.ok(JSONUtils.toJSON(rq.getSubject().getGrades()), ContentType.JSON, rq);
});
HttpHandler.registerPostRequestHandler("/topic-list", AccessLevel.STUDENT, (rq) -> {
return PostResponse.ok(JSONUtils.toJSON(rq.getSubject().getTopics(rq.getInt("grade"))), ContentType.JSON, rq);
});
HttpHandler.registerPostRequestHandler("/class-subjects", AccessLevel.ADMIN, (rq) -> {
SchoolClass schoolClass = rq.getSchoolClass();
if (schoolClass == null) return PostResponse.notFound("School class not found", rq);
List<Subject> subjects = schoolClass.getSubjects();
return PostResponse.ok(JSONUtils.toJSON(subjects, (subject, builder) -> {
builder.addProperty("id", subject.getId()).addProperty("name", subject.getName());
}), ContentType.JSON, rq);
});
HttpHandler.registerPostRequestHandler("/search-partner", AccessLevel.USER, (rq) -> {
SchoolClass schoolClass = rq.getSchoolClass();
Subject subject = rq.getSubject();
Topic topic = rq.getTopic();
Student student = rq.getCurrentStudent();
List<Student> students = Student.getAll().stream()
.filter((s) -> s.getSchoolClass().getGrade() == schoolClass.getGrade())
.filter((s) -> s.getCurrentTopic(subject).equals(topic)
&& s.getSelectedTasks().stream().filter((t) -> t.getTopic().equals(topic)).anyMatch((t) -> student.getSelectedTasks().contains(t))
&& s.getCurrentRequests(subject).stream().anyMatch((r) -> r == SubjectRequest.PARTNER))
.toList();
return PostResponse.ok(JSONUtils.toJSON(students, (partner, builder) -> {
builder.addProperty("id", partner.getId())
.addProperty("name", partner.getFirstName() + " " + partner.getLastName())
.addProperty("room", partner.getCurrentRoom() != null ? partner.getCurrentRoom().getLabel() : "None");
}), ContentType.JSON, rq);
});
HttpHandler.registerPostRequestHandler("/delete-subject", AccessLevel.ADMIN, (rq) ->
handleObjectAction(rq, new TypeToken<Subject>() {}, PostResponse.redirect("/manage_subjects", rq), (subject) -> subject.delete())
);
HttpHandler.registerPostRequestHandler("/edit-subject", AccessLevel.ADMIN, (rq) ->
handleObjectAction(rq, new TypeToken<Subject>() {}, PostResponse.redirect("/manage_subjects", rq), (subject) -> subject.edit(prepare(rq.getString("name"))))
);
HttpHandler.registerPostRequestHandler("/delete-classs", AccessLevel.ADMIN, (rq) ->
handleObjectAction(rq, new TypeToken<SchoolClass>() {}, PostResponse.redirect("/manage_classes", rq), (schoolClass) -> schoolClass.delete())
);
HttpHandler.registerPostRequestHandler("/edit-class", AccessLevel.ADMIN, (rq) ->
handleObjectAction(rq, new TypeToken<SchoolClass>() {}, PostResponse.redirect("/manage_classes", rq), (schoolClass) -> schoolClass.edit(prepare(rq.getString("name")), rq.getInt("grade")))
);
HttpHandler.registerPostRequestHandler("/add-subject-to-class", AccessLevel.ADMIN, (rq) ->
handleObjectAction(rq, new TypeToken<SchoolClass>() {}, PostResponse.redirect("/class", rq), (schoolClass) -> schoolClass.addSubject(rq.getSubject()))
);
HttpHandler.registerPostRequestHandler("/add-grade-to-subject", AccessLevel.ADMIN, (rq) ->
handleObjectAction(rq, new TypeToken<Subject>() {}, PostResponse.redirect("/subject", rq), (subject) -> subject.addToGrade(rq.getInt("grade")))
);
HttpHandler.registerPostRequestHandler("/delete-grade-from-subject", AccessLevel.ADMIN, (rq) ->
handleObjectAction(rq, new TypeToken<Subject>() {}, PostResponse.redirect("/subject", rq), (subject) -> subject.removeFromGrade(rq.getInt("grade")))
);
HttpHandler.registerPostRequestHandler("/delete-topics", AccessLevel.ADMIN, (rq) ->
handleObjectAction(rq, new TypeToken<Subject>() {}, PostResponse.redirect("/subject", rq), (subject) -> subject.getTopics(rq.getInt("grade")).forEach((topic) -> {
try {
topic.delete();
} catch (SQLException e) {
throw new IllegalStateException(e);
}
}))
);
HttpHandler.registerPostRequestHandler("/add-class-to-teacher", AccessLevel.ADMIN, (rq) ->
handleObjectAction(rq, new TypeToken<Teacher>() {}, PostResponse.redirect("/teacher", rq), (teacher) -> teacher.addClass(rq.getSchoolClass()))
);
HttpHandler.registerPostRequestHandler("/add-subject-to-teacher", AccessLevel.ADMIN, (rq) ->
handleObjectAction(rq, new TypeToken<Teacher>() {}, PostResponse.redirect("/teacher", rq), (teacher) -> teacher.addSubject(rq.getSubject()))
);
HttpHandler.registerPostRequestHandler("/change-graduation-level", AccessLevel.ADMIN, (rq) ->
handleObjectAction(rq, new TypeToken<Student>() {}, PostResponse.ok("Successfully changed graduation level", ContentType.TEXT_PLAIN, rq), (student) -> student.changeGraduationLevel(rq.getInt("graduationLevel")))
);
HttpHandler.registerPostRequestHandler("/get-module", AccessLevel.USER, (rq) -> {
return PostResponse.ok(Registry.moduleRegistry().get(rq.getString("key")).toJSON(), ContentType.JSON, rq);
});
HttpHandler.registerPostRequestHandler("/toggle-module", AccessLevel.ADMIN, (rq) -> {
Registry.moduleRegistry().get(rq.getString("key")).toggle();
return PostResponse.ok("Module toggled", ContentType.TEXT_PLAIN, rq);
});
HttpHandler.registerPostRequestHandler("/toggle-module-setting", AccessLevel.ADMIN, (rq) -> {
String[] key = rq.getString("key").split(":");
Registry.moduleRegistry().get(key[0]).toggleSetting(key[1]);
return PostResponse.ok("Module setting toggled", ContentType.TEXT_PLAIN, rq);
});
HttpHandler.registerPostRequestHandler("/student-results-csv", AccessLevel.TEACHER, (rq) -> {
Student student = rq.getCurrentStudent();
if (student == null) return PostResponse.badRequest("There is no current student", rq);
return PostResponse.ok(student.getResultsCSV(), ContentType.CSV, rq);
});
HttpHandler.registerPostRequestHandler("/completed-tasks", AccessLevel.TEACHER, (rq) -> {
SchoolClass schoolClass = rq.getSchoolClass();
Subject subject = rq.getSubject();
if (schoolClass == null) return PostResponse.badRequest("No school class specified", rq);
if (subject == null) return PostResponse.badRequest("No subject specified", rq);
return PostResponse.ok(schoolClass.getCompletedTasksCSV(subject), ContentType.CSV, rq);
});
HttpHandler.registerPostRequestHandler("/class-results", AccessLevel.TEACHER, (rq) -> {
SchoolClass schoolClass = rq.getSchoolClass();
Subject subject = rq.getSubject();
if (schoolClass == null) return PostResponse.badRequest("No school class specified", rq);
if (subject == null) return PostResponse.badRequest("No subject specified", rq);
return PostResponse.ok(schoolClass.getResultsCSV(subject), ContentType.CSV, rq);
});
HttpHandler.registerPostRequestHandler("/grade-results", AccessLevel.ADMIN, (rq) -> {
int grade = rq.getInt("grade");
Subject subject = rq.getSubject();
return PostResponse.ok(SchoolClass.getResultsCSV(grade, subject), ContentType.CSV, rq);
});
}

(you can just do a stub method if you do not want to code the logic)
(registering post request handlers this way may be added in a future update, that's why there is "type": "GET")

Copy link
Member

@Schlaumeier5 Schlaumeier5 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice, ty :)

@Schlaumeier5 Schlaumeier5 merged commit 65b7dc9 into Learn-Monitor:main Jan 20, 2026
5 of 6 checks passed
@Sana9058 Sana9058 deleted the issue-177-editor branch January 21, 2026 04:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants