diff --git a/build.gradle.kts b/build.gradle.kts index 6259525..6da4cc2 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -5,7 +5,9 @@ plugins { } group = "igs-landstuhl" -version = "v1.0-PATCH-3" + +version = "v1.0.1-SNAPSHOT-1" + application { mainClass.set("de.igslandstuhl.database.Application") } @@ -19,8 +21,10 @@ dependencies { implementation("com.google.code.gson:gson:2.13.1") implementation("commons-codec:commons-codec:1.19.0") implementation("com.googlecode.owasp-java-html-sanitizer:owasp-java-html-sanitizer:20240325.1") + implementation("org.jline:jline:3.30.6") // for better console input handling - testImplementation("org.junit.jupiter:junit-jupiter:5.10.0") // using JUnit 5 (latest) + testImplementation("org.junit.jupiter:junit-jupiter:5.13.4") // using JUnit 5 (latest) + testRuntimeOnly("org.junit.platform:junit-platform-launcher") } tasks.test { diff --git a/src/main/java/de/igslandstuhl/database/Application.java b/src/main/java/de/igslandstuhl/database/Application.java index c78aa7f..58a25c8 100644 --- a/src/main/java/de/igslandstuhl/database/Application.java +++ b/src/main/java/de/igslandstuhl/database/Application.java @@ -7,10 +7,13 @@ import de.igslandstuhl.database.api.SerializationException; import de.igslandstuhl.database.api.Subject; import de.igslandstuhl.database.api.Topic; +import de.igslandstuhl.database.api.modules.WebModule; import de.igslandstuhl.database.holidays.Holiday; import de.igslandstuhl.database.server.Server; import de.igslandstuhl.database.server.commands.Command; -import de.igslandstuhl.database.server.webserver.PostRequestHandler; +import de.igslandstuhl.database.server.webserver.WebPath; +import de.igslandstuhl.database.server.webserver.handlers.GetRequestHandler; +import de.igslandstuhl.database.server.webserver.handlers.PostRequestHandler; import de.igslandstuhl.database.utils.CommandLineUtils; /** @@ -92,18 +95,24 @@ public Topic[] readFile(String file) throws SerializationException, SQLException public static void main(String[] args) throws Exception { instance = new Application(args); + + if (!getInstance().suppressCmd()) { + Command.registerCommands(); + CommandLineUtils.setup(); + } + Server.getInstance().getConnection().createTables(); Holiday.setupCurrentSchoolYear(); PostRequestHandler.registerHandlers(); + WebModule.registerModules(); + + WebPath.registerPaths(); + GetRequestHandler.getInstance().registerHandlers(); if (getInstance().runsWebServer()) { Server.getInstance().getWebServer().start(); } - if (!getInstance().suppressCmd()) { - Command.registerCommands(); - CommandLineUtils.setup(); - } while (true) { if (!getInstance().suppressCmd()) { diff --git a/src/main/java/de/igslandstuhl/database/Registry.java b/src/main/java/de/igslandstuhl/database/Registry.java index 2ec4389..de0e8f9 100644 --- a/src/main/java/de/igslandstuhl/database/Registry.java +++ b/src/main/java/de/igslandstuhl/database/Registry.java @@ -5,15 +5,21 @@ import java.util.Map; import java.util.stream.Stream; +import de.igslandstuhl.database.api.modules.WebModule; import de.igslandstuhl.database.server.commands.Command; +import de.igslandstuhl.database.server.commands.CommandDescription; +import de.igslandstuhl.database.server.webserver.WebPath; +import de.igslandstuhl.database.server.webserver.handlers.HttpHandler; import de.igslandstuhl.database.server.webserver.requests.APIPostRequest; import de.igslandstuhl.database.server.webserver.requests.GetRequest; -import de.igslandstuhl.database.server.webserver.requests.HttpHandler; public class Registry implements Closeable { private static final Registry COMMAND_REGISTRY = new Registry<>(); + private static final Registry COMMAND_DESCRIPTION_REGISTRY = new Registry<>(); private static final Registry> POST_HANDLER_REGISTRY = new Registry<>(); private static final Registry> GET_HANDLER_REGISTRY = new Registry<>(); + private static final Registry MODULE_REGISTRY = new Registry<>(); + private static final Registry WEB_PATH_REGISTRY = new Registry<>(); public static Registry commandRegistry() { return COMMAND_REGISTRY; } @@ -23,6 +29,15 @@ public static Registry> postRequestHandlerRe public static Registry> getRequestHandlerRegistry() { return GET_HANDLER_REGISTRY; } + public static Registry moduleRegistry() { + return MODULE_REGISTRY; + } + public static Registry commandDescriptionRegistry() { + return COMMAND_DESCRIPTION_REGISTRY; + } + public static Registry webPathRegistry() { + return WEB_PATH_REGISTRY; + } private final Map objects = new HashMap<>(); diff --git a/src/main/java/de/igslandstuhl/database/api/Room.java b/src/main/java/de/igslandstuhl/database/api/Room.java index 79732b4..9919a6c 100644 --- a/src/main/java/de/igslandstuhl/database/api/Room.java +++ b/src/main/java/de/igslandstuhl/database/api/Room.java @@ -101,6 +101,9 @@ public static Room getRoom(String label) { } else { try { Room room = Server.getInstance().processSingleRequest(Room::fromSQLFields, "get_room_by_label", SQL_FIELDS, label); + if (room == null) { + return null; + } rooms.put(label, room); return room; } catch (SQLException e) { diff --git a/src/main/java/de/igslandstuhl/database/api/SchoolClass.java b/src/main/java/de/igslandstuhl/database/api/SchoolClass.java index 7139014..90485d9 100644 --- a/src/main/java/de/igslandstuhl/database/api/SchoolClass.java +++ b/src/main/java/de/igslandstuhl/database/api/SchoolClass.java @@ -2,7 +2,9 @@ import java.sql.SQLException; import java.util.ArrayList; +import java.util.LinkedList; import java.util.List; +import java.util.Locale; import java.util.Objects; import java.util.stream.Collectors; @@ -326,5 +328,80 @@ public boolean equals(Object obj) { public String toJSON() { return "{\"id\": " + id + ", \"label\": \"" + label + "\", \"grade\": " + grade + "}"; } - + + /** + * Generates a CSV representation of completed tasks for each student in the class for a given subject. + * @param subject the subject for which to generate the CSV + * @return a CSV string representing the completed tasks of each student for this subject + */ + public String getCompletedTasksCSV(Subject subject) { + StringBuilder csvBuilder = new StringBuilder(); + List students = getStudents(); + List tasks = new LinkedList<>(); + subject.getTopics(grade).forEach((t) -> tasks.addAll(t.getTasks())); + csvBuilder.append("Student ID,First Name,Last Name,Email"); + tasks.forEach((t) -> csvBuilder.append(",").append(t.getNumber())); + csvBuilder.append("\n"); + for (Student student : students) { + csvBuilder.append(student.getId()).append(",") + .append(student.getFirstName()).append(",") + .append(student.getLastName()).append(",") + .append(student.getEmail()); + for (Task task : tasks) { + boolean completed = student.hasCompletedTask(task); + csvBuilder.append(",").append(completed ? "1" : "0"); + } + csvBuilder.append("\n"); + } + return csvBuilder.toString(); + } + + /** + * Generates a CSV representation of the results for each student in the class for a given subject. + * @param subject the subject for which to generate the CSV + * @return a CSV string representing the results of each student for this subject + */ + public String getResultsCSV(Subject subject) { + StringBuilder csvBuilder = new StringBuilder(); + List students = getStudents(); + + csvBuilder.append("Student ID,First Name,Last Name,Email,Total Score(%)\n"); + for (Student student : students) { + double totalScore = student.getCurrentProgress(subject) * 100.; + csvBuilder.append(student.getId()).append(",") + .append(student.getFirstName()).append(",") + .append(student.getLastName()).append(",") + .append(student.getEmail()).append(",") + .append(String.format(Locale.US, "%.2f", totalScore)).append("\n"); + } + + return csvBuilder.toString(); + } + /** + * Generates a CSV representation of the results for all students in all classes of a given grade for a given subject. + * @param grade the grade level for which to generate the CSV + * @param subject the subject for which to generate the CSV + * @return a CSV string representing the results of each student for this subject across all classes of the specified grade + */ + public static String getResultsCSV(int grade, Subject subject) { + StringBuilder csvBuilder = new StringBuilder(); + List classes = getAll().stream().filter(c -> c.getGrade() == grade).toList(); + + csvBuilder.append("Class,Student ID,First Name,Last Name,Email,Total Score(%)\n"); + for (SchoolClass schoolClass : classes) { + List students = schoolClass.getStudents(); + for (Student student : students) { + double totalScore = student.getCurrentProgress(subject) * 100.; + csvBuilder.append(schoolClass.getLabel()).append(",") + .append(student.getId()).append(",") + .append(student.getFirstName()).append(",") + .append(student.getLastName()).append(",") + .append(student.getEmail()).append(",") + .append(String.format(Locale.US, "%.2f", totalScore)).append("\n"); + } + } + + return csvBuilder.toString(); + } + } diff --git a/src/main/java/de/igslandstuhl/database/api/Student.java b/src/main/java/de/igslandstuhl/database/api/Student.java index 77feb1f..5ccc5aa 100644 --- a/src/main/java/de/igslandstuhl/database/api/Student.java +++ b/src/main/java/de/igslandstuhl/database/api/Student.java @@ -5,6 +5,7 @@ import java.util.HashMap; import java.util.HashSet; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.Objects; import java.util.Random; @@ -404,6 +405,15 @@ public Set getCurrentRequests(Subject subject) { */ public SchoolClass getSchoolClass() { return schoolClass; } + /** + * Checks if the student has completed a specific task. + * @param task the task to check + * @return true if the student has completed the task, false otherwise + */ + public boolean hasCompletedTask(Task task) { + return completedTasks.contains(task); + } + /** * Adds a subject request for this student. * @param subjectId the subject ID @@ -801,4 +811,53 @@ public Student setPassword(String password) throws SQLException { students.remove(id); return get(id); } + + /** + * Generates a CSV string containing the student's results. + * @return a CSV string of the student's results + */ + public String getResultsCSV() { + StringBuilder csvBuilder = new StringBuilder(); + csvBuilder.append("Subject,Current Progress (%),Predicted Progress (%),Currently Achieved Grade,Predicted Grade\n"); + for (Subject subject : getSchoolClass().getSubjects()) { + double currentProgress = getCurrentProgress(subject) * 100; + double predictedProgress = getPredictedProgress(subject) * 100; + int currentGrade = getCurrentlyAchievedGrade(subject); + int predictedGrade = getPredictedGrade(subject); + csvBuilder.append(String.format(Locale.US, "%s,%.2f,%.2f,%d,%d\n", + subject.getName(), + currentProgress, + predictedProgress, + currentGrade, + predictedGrade)); + } + return csvBuilder.toString(); + } + /** + * Generates a CSV string containing all students' results. + * @return a CSV string of all students' results + */ + public static String getAllResultsCSV() { + StringBuilder csvBuilder = new StringBuilder(); + csvBuilder.append("Student ID,First Name,Last Name,Email,Subject,Current Progress (%),Predicted Progress (%),Currently Achieved Grade,Predicted Grade\n"); + for (Student student : getAll()) { + for (Subject subject : student.getSchoolClass().getSubjects()) { + double currentProgress = student.getCurrentProgress(subject) * 100; + double predictedProgress = student.getPredictedProgress(subject) * 100; + int currentGrade = student.getCurrentlyAchievedGrade(subject); + int predictedGrade = student.getPredictedGrade(subject); + csvBuilder.append(String.format(Locale.US, "%d,%s,%s,%s,%s,%.2f,%.2f,%d,%d\n", + student.getId(), + student.getFirstName(), + student.getLastName(), + student.getEmail(), + subject.getName(), + currentProgress, + predictedProgress, + currentGrade, + predictedGrade)); + } + } + return csvBuilder.toString(); + } } diff --git a/src/main/java/de/igslandstuhl/database/api/modules/BoolSetting.java b/src/main/java/de/igslandstuhl/database/api/modules/BoolSetting.java new file mode 100644 index 0000000..556ec3e --- /dev/null +++ b/src/main/java/de/igslandstuhl/database/api/modules/BoolSetting.java @@ -0,0 +1,31 @@ +package de.igslandstuhl.database.api.modules; + +public class BoolSetting extends ModuleSetting { + public BoolSetting(String key, String name, String description, boolean defaultValue) { + super(key, name, description, defaultValue); + } + + public void toggle() { + setValue(!getValue()); + } + public void enable() { + setValue(true); + } + public void disable() { + setValue(false); + } + public boolean isEnabled() { + return getValue(); + } + + @Override + public String toJSON() { + return "{" + + "\"key\":\"" + getKey() + "\"," + + "\"name\":\"" + getName() + "\"," + + "\"description\":\"" + getDescription() + "\"," + + "\"defaultValue\":" + getDefaultValue() + "," + + "\"value\":" + getValue() + + "}"; + } +} diff --git a/src/main/java/de/igslandstuhl/database/api/modules/ModuleSetting.java b/src/main/java/de/igslandstuhl/database/api/modules/ModuleSetting.java new file mode 100644 index 0000000..59df0d9 --- /dev/null +++ b/src/main/java/de/igslandstuhl/database/api/modules/ModuleSetting.java @@ -0,0 +1,49 @@ +package de.igslandstuhl.database.api.modules; + +public class ModuleSetting { + private final String key; + private final String name; + private final String description; + private final T defaultValue; + private T value; + + public ModuleSetting(String key, String name, String description, T defaultValue) { + this.key = key; + this.name = name; + this.description = description; + this.defaultValue = defaultValue; + this.value = defaultValue; + } + + public String getKey() { + return key; + } + public String getName() { + return name; + } + public String getDescription() { + return description; + } + public T getDefaultValue() { + return defaultValue; + } + + public T getValue() { + return value; + } + + public void setValue(T value) { + this.value = value; + } + + public String toJSON() { + return "{" + + "\"key\":\"" + key + "\"," + + "\"name\":\"" + name + "\"," + + "\"description\":\"" + description + "\"," + + "\"defaultValue\":\"" + defaultValue + "\"," + + "\"value\":\"" + value + "\"" + + "}"; + } + +} diff --git a/src/main/java/de/igslandstuhl/database/api/modules/WebModule.java b/src/main/java/de/igslandstuhl/database/api/modules/WebModule.java new file mode 100644 index 0000000..acf5492 --- /dev/null +++ b/src/main/java/de/igslandstuhl/database/api/modules/WebModule.java @@ -0,0 +1,137 @@ +package de.igslandstuhl.database.api.modules; + +import java.util.LinkedList; +import java.util.List; + +import de.igslandstuhl.database.Registry; + +public abstract class WebModule { + private String id; + private String name; + private String description; + + private boolean enabled; + + private List> settings = new LinkedList<>(); + + public WebModule(String id, String name, String description) { + this.id = id; + this.name = name; + this.description = description; + this.enabled = true; + } + + public String getId() { + return id; + } + public String getName() { + return name; + } + public String getDescription() { + return description; + } + public boolean isEnabled() { + return enabled; + } + + public String toJSON() { + StringBuilder sb = new StringBuilder(); + sb.append("{"); + sb.append("\"id\":\"").append(id).append("\","); + sb.append("\"name\":\"").append(name).append("\","); + sb.append("\"description\":\"").append(description).append("\","); + sb.append("\"enabled\":").append(enabled).append(","); + sb.append("\"settings\":{"); + for (int i = 0; i < settings.size(); i++) { + sb.append("\"").append(settings.get(i).getKey()).append("\":").append(settings.get(i).toJSON()); + if (i < settings.size() - 1) { + sb.append(","); + } + } + sb.append("}"); + sb.append("}"); + return sb.toString(); + } + + public List> getSettings() { + return settings; + } + public ModuleSetting getSettingByKey(String key) { + for (ModuleSetting setting : settings) { + if (setting.getKey().equals(key)) { + return setting; + } + } + return null; + } + public BoolSetting getBoolSetting(String key) { + ModuleSetting setting = getSettingByKey(key); + if (setting instanceof BoolSetting) { + return (BoolSetting) setting; + } + return null; + } + + protected abstract void onEnable(); + protected abstract void onDisable(); + protected abstract void onLoad(); + + public void enable() { + if (enabled) return; + onEnable(); + enabled = true; + } + public void disable() { + if (!enabled) return; + onDisable(); + enabled = false; + } + public void toggle() { + if (enabled) { + disable(); + } else { + enable(); + } + } + public void load() { + onLoad(); + } + + public void toggleSetting(String key) { + getBoolSetting(key).toggle(); + } + + private static class DummyModule extends WebModule { + public DummyModule(String id, String name, String description, List> settings) { + super(id, name, description); + this.getSettings().addAll(settings); + } + + @Override + protected void onEnable() { + // Dummy enable logic + } + + @Override + protected void onDisable() { + // Dummy disable logic + } + + @Override + protected void onLoad() { + // Dummy load logic + } + } + + private static void registerModule(WebModule module) { + Registry.moduleRegistry().register(module.getId(), module); + } + public static void registerModules() { + registerModule(new DummyModule("result_view", "Student Results View", "The view displaying the student's current progress and prognoses for the final result", List.of( + new BoolSetting("show_prognosis", "Show Prognosis", "Whether to display the prognosis for the final result", true), + new BoolSetting("show_current_progress", "Show Current", "Whether to display the current progress to the subject (in percent)", true), + new BoolSetting("show_current_grade", "Show Currently Achieved Grade", "Whether to display the grade the student would achieve when they decide to immediately stop working", false) + ))); + } + +} diff --git a/src/main/java/de/igslandstuhl/database/server/Server.java b/src/main/java/de/igslandstuhl/database/server/Server.java index 454a623..0a08e8e 100644 --- a/src/main/java/de/igslandstuhl/database/server/Server.java +++ b/src/main/java/de/igslandstuhl/database/server/Server.java @@ -228,6 +228,8 @@ public String getSQLResource(String username, String resource) { return new HashSet<>(Subject.getAll()).toString(); } else if (resource.equals("classes")) { return new HashSet<>(SchoolClass.getAll()).toString(); + } else if (resource.equals("all-student-results")) { + return Student.getAllResultsCSV(); } else { return null; } diff --git a/src/main/java/de/igslandstuhl/database/server/WebServer.java b/src/main/java/de/igslandstuhl/database/server/WebServer.java index d68223e..875a1b0 100644 --- a/src/main/java/de/igslandstuhl/database/server/WebServer.java +++ b/src/main/java/de/igslandstuhl/database/server/WebServer.java @@ -22,14 +22,14 @@ import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; -import de.igslandstuhl.database.server.webserver.HttpHeader; -import de.igslandstuhl.database.server.webserver.PostRequestHandler; -import de.igslandstuhl.database.server.webserver.SessionManager; +import de.igslandstuhl.database.server.webserver.handlers.GetRequestHandler; +import de.igslandstuhl.database.server.webserver.handlers.PostRequestHandler; import de.igslandstuhl.database.server.webserver.requests.GetRequest; +import de.igslandstuhl.database.server.webserver.requests.HttpHeader; import de.igslandstuhl.database.server.webserver.requests.PostRequest; -import de.igslandstuhl.database.server.webserver.responses.GetResponse; import de.igslandstuhl.database.server.webserver.responses.HttpResponse; import de.igslandstuhl.database.server.webserver.responses.PostResponse; +import de.igslandstuhl.database.server.webserver.sessions.SessionManager; /** * A simple HTTPS web server that handles various requests related to student data. @@ -129,15 +129,8 @@ String readHeadersAsString(InputStream in) throws IOException { } void handleGet(String headerString, PrintStream out) { - SessionManager sessionManager = Server.getInstance().getWebServer().getSessionManager(); GetRequest get = new GetRequest(headerString, clientIp, secure); - GetResponse response; - if (!sessionManager.validateSession(get)) { - response = GetResponse.forbidden(get); - } else { - String user = sessionManager.getSessionUser(get).getUsername(); - response = GetResponse.getResource(get, get.toResourceLocation(user), user); - } + HttpResponse response = GetRequestHandler.getInstance().handleRequest(get); response.respond(out); } diff --git a/src/main/java/de/igslandstuhl/database/server/commands/Command.java b/src/main/java/de/igslandstuhl/database/server/commands/Command.java index 9771e15..795833d 100644 --- a/src/main/java/de/igslandstuhl/database/server/commands/Command.java +++ b/src/main/java/de/igslandstuhl/database/server/commands/Command.java @@ -2,6 +2,7 @@ import java.sql.SQLException; import java.util.Arrays; +import java.util.List; import java.util.Random; import de.igslandstuhl.database.Registry; @@ -12,6 +13,11 @@ @FunctionalInterface public interface Command { public String execute(String[] args); + public default CommandDescription getDescription() { + return Registry.commandDescriptionRegistry().get(Registry.commandDescriptionRegistry() + .keyStream().filter(k -> Registry.commandRegistry().get(k) == this) + .findAny().orElse(null)); + } public static String executeCommand(String command, String[] args) { try { @@ -23,18 +29,19 @@ public static String executeCommand(String command, String[] args) { return ""; } } - public static void registerCommand(String name, Command command) { + public static void registerCommand(String name, Command command, CommandDescription description) { Registry.commandRegistry().register(name, command); + Registry.commandDescriptionRegistry().register(name, description); } public static void registerCommands() { registerCommand("exit", (args) -> { System.out.println("Exiting..."); System.exit(0); return ""; - }); + }, new CommandDescription("exit", "Exits the application", "exit")); registerCommand("help", (args) -> { - return Registry.commandRegistry().keyStream().reduce("Available Commands:", (s1,s2) -> s1+"\n"+s2); - }); + return Registry.commandRegistry().keyStream().reduce("Available Commands:", (s1,s2) -> s1+"\n"+Registry.commandDescriptionRegistry().get(s2).toString()); + }, new CommandDescription("help", "Displays this help message", "help")); // Add admin registerCommand("add-admin", (args) -> { if (args.length < 2) return "Usage: add-admin [username] [password]"; @@ -46,7 +53,7 @@ public static void registerCommands() { return "Error while trying to add admin:\n" + CommonUtils.getStacktrace(e); } return "Successfully added admin"; - }); + }, new CommandDescription("add-admin", "Adds a new admin", "add-admin [username] [password]")); registerCommand("remove-admin", (args) -> { if (args.length < 1) return "Usage: remove-admin [username]"; try { @@ -57,7 +64,7 @@ public static void registerCommands() { return "Error while trying to remove admin:\n" + CommonUtils.getStacktrace(e); } return "Successfully removed admin"; - }); + }, new CommandDescription("remove-admin", "Removes an admin", "remove-admin [username]")); registerCommand("get-level-ratio", (args) -> { if (args.length < 1) return "Usage: get-level-ratio [level]"; TaskLevel level; @@ -69,7 +76,7 @@ public static void registerCommands() { return e.getMessage(); } return String.valueOf(level.getRatio() * 100) + "%"; - }); + }, new CommandDescription("get-level-ratio", "Gets the ratio of a task level", "get-level-ratio [level]")); // Room commands registerCommand("list-rooms", (args) -> { @@ -79,13 +86,13 @@ public static void registerCommands() { return "Error while trying to access database:\n" + CommonUtils.getStacktrace(e); } return Room.getRooms().keySet().stream().reduce("Rooms:", (s1, s2) -> s1 + "\n" + s2); - }); + }, new CommandDescription("list-rooms", "Lists all available rooms", "list-rooms")); registerCommand("get-room-level", (args) -> { if (args.length < 1) return "Usage: get-room-level [room]"; Room room = Room.getRoom(argsPart(args, 0, args.length)); if (room == null) return "Room not found. Try list-rooms for a list of available rooms"; return "Room " + room.getLabel() + " has access level " + room.getMinimumLevel(); - }); + }, new CommandDescription("get-room-level", "Gets the minimum level required to access a room", "get-room-level [room]")); registerCommand("set-room-level", (args) -> { if (args.length < 2) return "Usage: set-room-level [room] [level]"; Room room = Room.getRoom(argsPart(args, 0, args.length-1)); @@ -104,7 +111,7 @@ public static void registerCommands() { return e.getMessage(); } return "Successfully changed room level"; - }); + }, new CommandDescription("set-room-level", "Sets the minimum level required to access a room", "set-room-level [room] [level]")); registerCommand("add-room", (args) -> { if (args.length < 2) return "Usage: add-room [room] [level]"; if (Room.getRoom(args[0]) != null) return "Room already present"; @@ -122,7 +129,7 @@ public static void registerCommands() { throw new IllegalStateException(e); } return "Room successfully added"; - }); + }, new CommandDescription("add-room", "Adds a new room", "add-room [room] [level]")); registerCommand("remove-room", (args) -> { if (args.length < 1) return "Usage: remove-room [room]"; @@ -133,13 +140,13 @@ public static void registerCommands() { return "Error while trying to access database: \n" + CommonUtils.getStacktrace(e); } return "Room successfully deleted"; - }); + }, new CommandDescription("remove-room", "Removes a room", "remove-room [room]")); // class commands registerCommand("list-classes", (args) -> { return SchoolClass.getAll().stream().map(SchoolClass::getLabel).reduce("Classes:", (s1,s2) -> s1 + "\n" + s2); - }); + }, new CommandDescription("list-classes", "Lists all classes", "list-classes")); registerCommand("add-subject-to-class", (args) -> { if (args.length != 2) return "Usage: add-subject-to-class [subject] [class]"; @@ -155,7 +162,7 @@ public static void registerCommands() { throw new IllegalStateException(e); } return "Successfully added subject to class"; - }); + }, new CommandDescription("add-subject-to-class", "Adds a subject to a class", "add-subject-to-class [subject] [class]")); registerCommand(("set-class-label"), (args) -> { if (args.length != 2) return "Usage: set-class-label [old] [new]"; @@ -170,7 +177,7 @@ public static void registerCommands() { } return "Class label successfully updated."; - }); + }, new CommandDescription("set-class-label", "Sets the label of a class", "set-class-label [old] [new]")); registerCommand(("set-class-grade"), (args) -> { if (args.length != 2) return "Usage: set-class-grade [class] [grade]"; @@ -191,12 +198,12 @@ public static void registerCommands() { } return "Class grade successfully updated."; - }); + }, new CommandDescription("set-class-grade", "Sets the grade of a class", "set-class-grade [class] [grade]")); registerCommand("add-class", (args) -> { if (args.length != 1) return "Usage: add-class [class]"; SchoolClass schoolClass = SchoolClass.getOrCreate(args[0]); return "Successfully created class " + schoolClass.getLabel() + " of grade " + schoolClass.getGrade(); - }); + }, new CommandDescription("add-class", "Adds a new class", "add-class [class]")); registerCommand("remove-class", (args) -> { if (args.length != 1) return "Usage: remove-class [class]"; SchoolClass schoolClass = SchoolClass.get(args[0]); @@ -207,21 +214,21 @@ public static void registerCommands() { throw new IllegalStateException(e); } return "Successfully deleted class"; - }); + }, new CommandDescription("remove-class", "Removes a class", "remove-class [class]")); // school year registerCommand("list-school-years", (args) -> { return SchoolYear.getAll().stream().map(SchoolYear::getLabel).reduce("School years:", (s1,s2) -> s1 + "\n" + s2); - }); + }, new CommandDescription("list-school-years", "Lists all school years", "list-school-years")); registerCommand("get-current-school-year", (args) -> { SchoolYear current = SchoolYear.getCurrentYear(); if (current == null) return "No school year found."; return "Current school year: " + current.getLabel() + ", current week: " + current.getCurrentWeek() + ", total week count: " + current.getWeekCount(); - }); + }, new CommandDescription("get-current-school-year", "Gets the current school year", "get-current-school-year")); registerCommand("get-current-week", (args) -> { SchoolYear current = SchoolYear.getCurrentYear(); if (current == null) return "No school year found."; return "Current week: " + current.getCurrentWeek(); - }); + }, new CommandDescription("get-current-week", "Gets the current week", "get-current-week")); registerCommand("set-current-week", (args) -> { if (args.length != 1) return "Usage: set-current-week [week]"; int week; @@ -239,7 +246,7 @@ public static void registerCommands() { throw new IllegalStateException(e); } return "Week successfully changed"; - }); + }, new CommandDescription("set-current-week", "Sets the current week", "set-current-week [week]")); registerCommand("inc-week", (args) -> { SchoolYear current = SchoolYear.getCurrentYear(); if (current == null) return "No school year found"; @@ -249,7 +256,7 @@ public static void registerCommands() { throw new IllegalStateException(e); } return "Week successfully changed"; - }); + }, new CommandDescription("inc-week", "Increases the current week by 1", "inc-week")); registerCommand("add-school-year", (args) -> { if (args.length != 2) return "Usage: add-school-year [label] [week count]"; try { @@ -260,7 +267,7 @@ public static void registerCommands() { throw new IllegalStateException(e); } return "Successfully added school year"; - }); + }, new CommandDescription("add-school-year", "Adds a new school year", "add-school-year [label] [week count]")); registerCommand("remove-school-year", (args) -> { if (args.length != 1) return "Usage: remove-school-year [label]"; SchoolYear schoolYear = SchoolYear.get(args[0]); @@ -271,7 +278,7 @@ public static void registerCommands() { throw new IllegalStateException(e); } return "School Year successfully removed"; - }); + }, new CommandDescription("remove-school-year", "Removes a school year", "remove-school-year [label]")); // User commands registerCommand("regenerate-user-password", (args) -> { if (args.length != 1) return "Usage: regenerate-user-password [user]"; @@ -282,10 +289,10 @@ public static void registerCommands() { } catch (SQLException e) { throw new IllegalStateException(e); } - }); + }, new CommandDescription("regenerate-user-password", "Regenerates a user's password", "regenerate-user-password [user]")); registerCommand("generate-password", (args) -> { return "Passwort: " + User.generateRandomPassword(12, CommonUtils.stringToSeed(argsPart(args, 0, args.length)) + System.currentTimeMillis() + new Random().nextInt(1000)); - }); + }, new CommandDescription("generate-password", "Generates a random password", "generate-password [optional seed]")); registerCommand("change-user-password", (args) -> { if (args.length != 2) return "Usage: change-user-password [user] [password]"; User user = User.getUser(args[0]); @@ -296,11 +303,11 @@ public static void registerCommands() { } catch (SQLException e) { throw new IllegalStateException(e); } - }); + }, new CommandDescription("change-user-password", "Changes a user's password", "change-user-password [user] [password]")); // Subject commands registerCommand("list-subjects", (args) -> { return Subject.getAll().stream().map(Subject::getName).reduce("Subjects:", (s1,s2) -> s1+"\n"+s2); - }); + }, new CommandDescription("list-subjects", "Lists all subjects", "list-subjects")); registerCommand("add-subject", (args) -> { if (args.length < 1) return "Usage: add-subject [subject]"; String name = argsPart(args, 0, args.length); @@ -310,7 +317,7 @@ public static void registerCommands() { } catch (SQLException e) { throw new IllegalStateException(e); } - }); + }, new CommandDescription("add-subject", "Adds a new subject", "add-subject [subject]")); registerCommand("rename-subject", (args) -> { if (args.length < 3) return "Usage: rename-subject [old name] : [new name]"; @@ -327,7 +334,7 @@ public static void registerCommands() { } catch (SQLException e) { throw new IllegalStateException(e); } - }); + }, new CommandDescription("rename-subject", "Renames a subject", "rename-subject [old name] : [new name]")); registerCommand("remove-subject", (args) -> { if (args.length == 0) return "remove-subject [name]"; String name = argsPart(args, 0, args.length); @@ -339,7 +346,7 @@ public static void registerCommands() { } catch (SQLException e) { throw new IllegalStateException(e); } - }); + }, new CommandDescription("remove-subject", "Removes a subject", "remove-subject [name]")); // manual sql commands registerCommand("sql-update", (args) -> { String command = argsPart(args, 0, args.length); @@ -349,9 +356,12 @@ public static void registerCommands() { throw new IllegalArgumentException(e); } return "Successfully executed"; - }); + }, new CommandDescription("sql-update", "Executes an update sql command", "sql-update [command]")); } private static String argsPart(String[] args, int start, int end) { return Arrays.stream(Arrays.copyOfRange(args, start, end)).reduce("", (s1,s2) -> s1+" "+s2).replaceFirst(" ", ""); } + public static List getAllCommandNames() { + return Registry.commandRegistry().keyStream().toList(); + } } diff --git a/src/main/java/de/igslandstuhl/database/server/commands/CommandDescription.java b/src/main/java/de/igslandstuhl/database/server/commands/CommandDescription.java new file mode 100644 index 0000000..ae5a8bb --- /dev/null +++ b/src/main/java/de/igslandstuhl/database/server/commands/CommandDescription.java @@ -0,0 +1,9 @@ +package de.igslandstuhl.database.server.commands; + +import de.igslandstuhl.database.utils.StringUtils; + +public record CommandDescription(String name, String description, String usage) { + public String toString() { + return StringUtils.fitSize(name, 20) + "\t" + StringUtils.fitSize(description, 50) + "\t" + StringUtils.fitSize(usage, 30); + } +} diff --git a/src/main/java/de/igslandstuhl/database/server/resources/ResourceHelper.java b/src/main/java/de/igslandstuhl/database/server/resources/ResourceHelper.java index 81a689e..bfaf82e 100644 --- a/src/main/java/de/igslandstuhl/database/server/resources/ResourceHelper.java +++ b/src/main/java/de/igslandstuhl/database/server/resources/ResourceHelper.java @@ -17,12 +17,16 @@ import java.util.Enumeration; import java.util.Iterator; import java.util.List; +import java.util.Map; import java.util.regex.Pattern; import java.util.stream.Stream; import java.util.zip.ZipEntry; import java.util.zip.ZipException; import java.util.zip.ZipFile; +import com.google.gson.Gson; +import com.google.gson.reflect.TypeToken; + import de.igslandstuhl.database.server.Server; /** @@ -276,4 +280,13 @@ public static String readVirtualResource(String user, ResourceLocation location) return null; } } + + public static Map readJsonResourceAsMap(ResourceLocation location) throws IOException { + try (BufferedReader in = new BufferedReader(new InputStreamReader(openResourceAsStream(location), StandardCharsets.UTF_8))) { + Gson gson = new Gson(); + java.lang.reflect.Type mapType = new TypeToken>(){}.getType(); + Map json = gson.fromJson(in, mapType); + return json; + } + } } \ No newline at end of file diff --git a/src/main/java/de/igslandstuhl/database/server/sql/SQLHelper.java b/src/main/java/de/igslandstuhl/database/server/sql/SQLHelper.java index 1828cb0..4be82d9 100644 --- a/src/main/java/de/igslandstuhl/database/server/sql/SQLHelper.java +++ b/src/main/java/de/igslandstuhl/database/server/sql/SQLHelper.java @@ -81,22 +81,22 @@ public static SQLProcess getQueryProcess(String queryName, String... args) { } /** - * Gets an SQL add statement for a specific object and replaces placeholders with provided arguments. - * - * @param object the name of the object to add (e.g., "student", "course") - * @param args the arguments to replace in the SQL statement - * @return the SQL add statement as a String with placeholders replaced + * Gets an SQL statement for a specific type and object. + * @param type the type of SQL operation (e.g., "add", "delete", "update") + * @param object the name of the object (e.g., "student", "course") + * @return the SQL statement as a String */ - public static String getSQLAddStatement(String object) { - ResourceLocation location = new ResourceLocation(CONTEXT, PUSHES, "add_" + object + ".sql"); + private static String getSQLStatement(String type, String object) { + ResourceLocation location = new ResourceLocation(CONTEXT, PUSHES, type + "_" + object + ".sql"); String statement; try { statement = ResourceHelper.readResourceCompletely(location); } catch (FileNotFoundException e) { - throw new SQLCommandNotFoundException("add_" + object, e); + throw new SQLCommandNotFoundException(type + "_" + object, e); } return statement; } + /** * Gets an SQL process for adding an object to the database. * @@ -105,25 +105,7 @@ public static String getSQLAddStatement(String object) { * @return a SQLVoidProcess that executes the add statement */ public static SQLVoidProcess getAddObjectProcess(String object, String... args) { - return SQLVoidProcess.update(getSQLAddStatement(object), args); - } - - /** - * Gets an SQL delete statement for a specific object and replaces placeholders with provided arguments. - * - * @param object the name of the object to add (e.g., "student", "course") - * @param args the arguments to replace in the SQL statement - * @return the SQL add statement as a String with placeholders replaced - */ - public static String getSQLDeleteStatement(String object) { - ResourceLocation location = new ResourceLocation(CONTEXT,PUSHES, "delete_" + object + ".sql"); - String statement; - try { - statement = ResourceHelper.readResourceCompletely(location); - } catch (FileNotFoundException e) { - throw new SQLCommandNotFoundException("delete_" + object, e); - } - return statement; + return SQLVoidProcess.update(getSQLStatement("add", object), args); } /** @@ -134,25 +116,7 @@ public static String getSQLDeleteStatement(String object) { * @return a SQLVoidProcess that executes the add statement */ public static SQLVoidProcess getDeleteObjectProcess(String object, String... args) { - return SQLVoidProcess.update(getSQLDeleteStatement(object), args); - } - - /** - * Gets an SQL delete statement for a specific object and replaces placeholders with provided arguments. - * - * @param object the name of the object to add (e.g., "student", "course") - * @param args the arguments to replace in the SQL statement - * @return the SQL add statement as a String with placeholders replaced - */ - public static String getSQLUpdateStatement(String object) { - ResourceLocation location = new ResourceLocation(CONTEXT,PUSHES, "update_" + object + ".sql"); - String statement; - try { - statement = ResourceHelper.readResourceCompletely(location); - } catch (FileNotFoundException e) { - throw new SQLCommandNotFoundException("update_" + object, e); - } - return statement; + return SQLVoidProcess.update(getSQLStatement("delete", object), args); } /** @@ -163,6 +127,6 @@ public static String getSQLUpdateStatement(String object) { * @return a SQLVoidProcess that executes the add statement */ public static SQLVoidProcess getUpdateObjectProcess(String object, String... args) { - return SQLVoidProcess.update(getSQLUpdateStatement(object), args); + return SQLVoidProcess.update(getSQLStatement("update", object), args); } } diff --git a/src/main/java/de/igslandstuhl/database/server/webserver/AccessManager.java b/src/main/java/de/igslandstuhl/database/server/webserver/AccessManager.java index 3cb2a86..02c0b33 100644 --- a/src/main/java/de/igslandstuhl/database/server/webserver/AccessManager.java +++ b/src/main/java/de/igslandstuhl/database/server/webserver/AccessManager.java @@ -1,8 +1,12 @@ package de.igslandstuhl.database.server.webserver; +import java.io.IOException; import java.util.Arrays; +import java.util.List; +import java.util.Map; import de.igslandstuhl.database.api.User; +import de.igslandstuhl.database.server.resources.ResourceHelper; import de.igslandstuhl.database.server.resources.ResourceLocation; /** @@ -10,33 +14,92 @@ * It determines whether a user has access to a specific resource based on predefined rules. */ public class AccessManager { + private static final AccessManager INSTANCE = new AccessManager(); + public static AccessManager getInstance() { + return INSTANCE; + } /** * Public spaces and locations that are accessible without authentication. * These resources can be accessed by anyone, regardless of their authentication status. */ - private static final String[] PUBLIC_SPACES = {"error", "site", "icons"}; + private final String[] PUBLIC_SPACES; /** * The user space is restricted to authenticated users. */ - private static final String USER_SPACE = "user"; + private final String USER_SPACE; /** * The teacher space is restricted to authenticated teachers. */ - private static final String TEACHER_SPACE = "teacher"; + private final String TEACHER_SPACE; /** * The admin space is restricted to authenticated admins. */ - private static final String ADMIN_SPACE = "admin"; + private final String ADMIN_SPACE; /** * Public locations that are accessible without authentication. * These resources can be accessed by anyone, regardless of their authentication status. */ - private static final String[] PUBLIC_LOCATIONS = {"rooms", "subjects"}; + private final String[] PUBLIC_LOCATIONS; + /** + * User locations that are accessible only to authenticated users. + * These resources require user login for access. + */ + private final String[] USER_LOCATIONS; + /** + * Teacher locations that are accessible only to authenticated teachers. + * These resources require teacher privileges for access. + */ + private final String[] TEACHER_LOCATIONS; /** * Admin locations that are accessible only to authenticated admins. * These resources require admin privileges for access. */ - private static final String[] ADMIN_LOCATIONS = {"students", "teachers", "classes"}; + private final String[] ADMIN_LOCATIONS; + + + @SuppressWarnings("unchecked") + private AccessManager() { + ResourceLocation metaLocation = new ResourceLocation("meta", "paths", "spaces.json"); + String userSpace = "user"; + String teacherSpace = "teacher"; + String adminSpace = "admin"; + + String[] publicSpaces = {"error", "site", "icons"}; + String[] publicLocations = {"rooms", "subjects"}; + String[] userLocations = {}; + String[] teacherLocations = {}; + String[] adminLocations = {"students", "teachers", "classes"}; + try { + Map pathData = ResourceHelper.readJsonResourceAsMap(metaLocation); + List publicSpacesList = (List) pathData.get("public_spaces"); + List publicLocationsList = (List) pathData.get("public_locations"); + List userLocationsList = (List) pathData.get("user_locations"); + List teacherLocationsList = (List) pathData.get("teacher_locations"); + List adminLocationsList = (List) pathData.get("admin_locations"); + userSpace = (String) pathData.get("user_space"); + teacherSpace = (String) pathData.get("teacher_space"); + adminSpace = (String) pathData.get("admin_space"); + publicSpaces = publicSpacesList.toArray(new String[publicSpacesList.size()]); + publicLocations = publicLocationsList.toArray(new String[publicLocationsList.size()]); + userLocations = userLocationsList.toArray(new String[userLocationsList.size()]); + teacherLocations = teacherLocationsList.toArray(new String[teacherLocationsList.size()]); + adminLocations = adminLocationsList.toArray(new String[adminLocationsList.size()]); + } catch (IOException e) { + System.err.println("Could not read spaces metadata!"); + e.printStackTrace(); + } finally { + USER_SPACE = userSpace; + TEACHER_SPACE = teacherSpace; + ADMIN_SPACE = adminSpace; + PUBLIC_SPACES = publicSpaces; + PUBLIC_LOCATIONS = publicLocations; + USER_LOCATIONS = userLocations; + TEACHER_LOCATIONS = teacherLocations; + ADMIN_LOCATIONS = adminLocations; + } + + + } /** * Checks if a user has access to a specific resource. @@ -45,10 +108,10 @@ public class AccessManager { * @param resource the ResourceLocation representing the resource to check access for * @return true if the user has access to the resource, false otherwise */ - public static boolean hasAccess(String user, ResourceLocation resource) { + public boolean hasAccess(String user, ResourceLocation resource) { return hasAccess(User.getUser(user), resource); } - public static boolean hasAccess(User user, ResourceLocation resource) { + public boolean hasAccess(User user, ResourceLocation resource) { if (Arrays.asList(PUBLIC_SPACES).contains(resource.namespace()) || Arrays.asList(PUBLIC_LOCATIONS).contains(resource.resource())) { return true; } else if (user != null) { diff --git a/src/main/java/de/igslandstuhl/database/server/webserver/WebPath.java b/src/main/java/de/igslandstuhl/database/server/webserver/WebPath.java new file mode 100644 index 0000000..24bd102 --- /dev/null +++ b/src/main/java/de/igslandstuhl/database/server/webserver/WebPath.java @@ -0,0 +1,32 @@ +package de.igslandstuhl.database.server.webserver; + +import java.io.IOException; +import java.util.List; +import java.util.Map; + +import de.igslandstuhl.database.Registry; +import de.igslandstuhl.database.server.resources.ResourceHelper; +import de.igslandstuhl.database.server.resources.ResourceLocation; +import de.igslandstuhl.database.server.webserver.requests.RequestType; + +public record WebPath(RequestType type, String handlerType, List namespaces, String context, AccessLevel accessLevel) { + public static void registerPath(String path, RequestType type, String handlerType, List namespaces, String context, AccessLevel accessLevel) { + Registry.webPathRegistry().register(path, new WebPath(type, handlerType, namespaces, context, accessLevel)); + } + public static void registerPaths() throws IOException { + if (Registry.webPathRegistry().stream().count() > 0) return; // already registered + ResourceLocation metaLocation = new ResourceLocation("meta", "paths", "get_paths.json"); + Map pathData = ResourceHelper.readJsonResourceAsMap(metaLocation); + pathData.keySet().forEach((path) -> { + @SuppressWarnings("unchecked") + Map pathInfo = (Map) pathData.get(path); + RequestType requestType = RequestType.valueOf((String) pathInfo.get("type")); + String handlerType = (String) pathInfo.get("handler_type"); + @SuppressWarnings("unchecked") + List namespaces = (List) pathInfo.get("namespaces"); + String context = (String) pathInfo.get("context"); + AccessLevel accessLevel = AccessLevel.valueOf(((String) pathInfo.get("access_level")).toUpperCase()); + registerPath(path, requestType, handlerType, namespaces, context, accessLevel); + }); + } +} diff --git a/src/main/java/de/igslandstuhl/database/server/webserver/WebResourceHandler.java b/src/main/java/de/igslandstuhl/database/server/webserver/WebResourceHandler.java deleted file mode 100644 index 8ef168a..0000000 --- a/src/main/java/de/igslandstuhl/database/server/webserver/WebResourceHandler.java +++ /dev/null @@ -1,98 +0,0 @@ -package de.igslandstuhl.database.server.webserver; - -import java.util.Arrays; - -import de.igslandstuhl.database.api.User; -import de.igslandstuhl.database.server.resources.ResourceLocation; - -/** - * Handles the mapping of web resource paths to resource locations. - * This class provides methods to determine if a path is a SQL web resource, - * and to convert paths into ResourceLocation objects. - */ -public final class WebResourceHandler { - private static final String[] SQL_WEB_RESOURCES = {"/mydata", "/rooms", "/mysubjects", "/myclasses", "/teachers", "/students", "/subjects", "/classes"}; - private static String[] userOnlySpace = {"dashboard", "build_dashboard.js", "results", "build_results.js", "partner_search", "build_partner_search.js"}; - private static String[] teacherOnlySpace = {"dashboard", "build_dashboard.js", "student", "build_student.js", "student-results", "build_results.js"}; - private static String[] adminOnlySpace = {"dashboard", "build_dashboard.js", "manage_students", "manage_teachers", "manage_classes", "manage_subjects", "manage_rooms", "student", "student-results", "teacher", "build_teacher.js", "room", "build_room.js", "subject", "build_subject.js", "class", "build_class.js", "teacher-classes", "teacher-subjects", "teacher-students", "teacher-results", "teacher-dashboard", "build_teacher_dashboard.js"}; - - private WebResourceHandler(){} - - private static boolean isSQLWebResource(String path) { - return Arrays.asList(SQL_WEB_RESOURCES).contains(path); - } - - private static boolean inUserOnlySpace(String path) { - for (String userOnlyPaths : userOnlySpace) { - if (path.replaceFirst("/", "").replace(".html", "").equals(userOnlyPaths)) { - return true; - } - } - return false; - } - private static boolean inTeacherOnlySpace(String path) { - for (String teacherOnlyPaths : teacherOnlySpace) { - if (path.replaceFirst("/", "").replace(".html", "").equals(teacherOnlyPaths)) { - return true; - } - } - return false; - } - private static boolean inAdminOnlySpace(String path) { - for (String adminOnlyPaths : adminOnlySpace) { - if (path.replaceFirst("/", "").replace(".html", "").equals(adminOnlyPaths)) { - return true; - } - } - return false; - } - - public static ResourceLocation locationFromPath(String path, User user) { - if (isSQLWebResource(path)) { - return new ResourceLocation("virtual", "sql", path.replaceFirst("/", "")); - } - if (path.equals("/")) { - path = "/index.html"; - } else if (path.endsWith(".ico")) { - path = "/icons" + path; - } - String[] parts = path.split("\\."); - - String context; - if (path.endsWith(".ico") || path.endsWith(".png")) { - context = "imgs"; - } else if (parts.length > 1) { - context = parts[1]; - } else { - context = "html"; - path += ".html"; - } - - if (user == null) user = User.ANONYMOUS; - - parts = path.split("/", 3); - String namespace; - String resource; - if (parts.length > 2) { - namespace = parts[1]; - resource = parts[2]; - } else { - if (inAdminOnlySpace(parts[1]) && (user == User.ANONYMOUS || user.isAdmin())) { - namespace = "admin"; - } else if (inTeacherOnlySpace(parts[1]) && (user == User.ANONYMOUS || user.isTeacher() || user.isAdmin())) { - namespace = "teacher"; - } else if (inUserOnlySpace(parts[1])) { - namespace = "user"; - } else if (inAdminOnlySpace(parts[1])) { - namespace = "admin"; - } else if (inTeacherOnlySpace(parts[1])) { - namespace = "teacher"; - } else { - namespace = "site"; - } - resource = parts[1]; - } - - return new ResourceLocation(context, namespace, resource); - } -} diff --git a/src/main/java/de/igslandstuhl/database/server/webserver/handlers/GetRequestHandler.java b/src/main/java/de/igslandstuhl/database/server/webserver/handlers/GetRequestHandler.java new file mode 100644 index 0000000..256a855 --- /dev/null +++ b/src/main/java/de/igslandstuhl/database/server/webserver/handlers/GetRequestHandler.java @@ -0,0 +1,60 @@ +package de.igslandstuhl.database.server.webserver.handlers; + +import java.util.List; + +import de.igslandstuhl.database.Registry; +import de.igslandstuhl.database.server.Server; +import de.igslandstuhl.database.server.webserver.WebPath; +import de.igslandstuhl.database.server.webserver.requests.GetRequest; +import de.igslandstuhl.database.server.webserver.requests.RequestType; +import de.igslandstuhl.database.server.webserver.responses.GetResponse; +import de.igslandstuhl.database.server.webserver.responses.HttpResponse; +import de.igslandstuhl.database.server.webserver.sessions.SessionManager; +import de.igslandstuhl.database.utils.ThrowingFunction; + +public class GetRequestHandler { + private static final GetRequestHandler instance = new GetRequestHandler(); + public static GetRequestHandler getInstance() { + return instance; + } + private GetRequestHandler() {} + + public final HttpResponse handleRequest(GetRequest request) { + SessionManager sessionManager = Server.getInstance().getWebServer().getSessionManager(); + if (!sessionManager.validateSession(request)) { + return GetResponse.forbidden(request); + } else { + String path = request.getPath(); + HttpHandler handler = Registry.getRequestHandlerRegistry().get(path); + return handler.handleHttpRequest(request); + } + } + + public static GetResponse handleFileRequest(GetRequest request) { + String user = Server.getInstance().getWebServer().getSessionManager().getSessionUser(request).getUsername(); + return GetResponse.getResource(request, request.toResourceLocation(user), user, false); + } + public static GetResponse handleTemplatingFileRequest(GetRequest request) { + String user = Server.getInstance().getWebServer().getSessionManager().getSessionUser(request).getUsername(); + return GetResponse.getResource(request, request.toResourceLocation(user), user, true); + } + public static GetResponse handleSQLRequest(GetRequest request) { + String user = Server.getInstance().getWebServer().getSessionManager().getSessionUser(request).getUsername(); + return GetResponse.getResource(request, request.toResourceLocation(user), user, false); + } + + public final void registerHandlers() { + if (Registry.getRequestHandlerRegistry().stream().count() > 0) return; // already registered + List getPaths = Registry.webPathRegistry().keyStream().filter((p) -> Registry.webPathRegistry().get(p).type() == RequestType.GET).toList(); + for (String path : getPaths) { + WebPath webPath = Registry.webPathRegistry().get(path); + ThrowingFunction handlerFunction = switch (webPath.handlerType()) { + case "FileRequestHandler" -> GetRequestHandler::handleFileRequest; + case "TemplatingFileRequestHandler" -> GetRequestHandler::handleTemplatingFileRequest; + case "SQLRequestHandler" -> GetRequestHandler::handleSQLRequest; + default -> throw new IllegalArgumentException("Unknown handler type: " + webPath.handlerType()); + }; + HttpHandler.registerGetRequestHandler(path, webPath.accessLevel(), handlerFunction); + } + } +} diff --git a/src/main/java/de/igslandstuhl/database/server/webserver/requests/HttpHandler.java b/src/main/java/de/igslandstuhl/database/server/webserver/handlers/HttpHandler.java similarity index 86% rename from src/main/java/de/igslandstuhl/database/server/webserver/requests/HttpHandler.java rename to src/main/java/de/igslandstuhl/database/server/webserver/handlers/HttpHandler.java index 1f46ace..2477732 100644 --- a/src/main/java/de/igslandstuhl/database/server/webserver/requests/HttpHandler.java +++ b/src/main/java/de/igslandstuhl/database/server/webserver/handlers/HttpHandler.java @@ -1,11 +1,14 @@ -package de.igslandstuhl.database.server.webserver.requests; +package de.igslandstuhl.database.server.webserver.handlers; import de.igslandstuhl.database.Registry; import de.igslandstuhl.database.server.Server; import de.igslandstuhl.database.server.webserver.AccessLevel; -import de.igslandstuhl.database.server.webserver.SessionManager; import de.igslandstuhl.database.server.webserver.Status; +import de.igslandstuhl.database.server.webserver.requests.APIPostRequest; +import de.igslandstuhl.database.server.webserver.requests.GetRequest; +import de.igslandstuhl.database.server.webserver.requests.HttpRequest; import de.igslandstuhl.database.server.webserver.responses.HttpResponse; +import de.igslandstuhl.database.server.webserver.sessions.SessionManager; import de.igslandstuhl.database.utils.ThrowingFunction; public class HttpHandler { diff --git a/src/main/java/de/igslandstuhl/database/server/webserver/PostRequestHandler.java b/src/main/java/de/igslandstuhl/database/server/webserver/handlers/PostRequestHandler.java similarity index 89% rename from src/main/java/de/igslandstuhl/database/server/webserver/PostRequestHandler.java rename to src/main/java/de/igslandstuhl/database/server/webserver/handlers/PostRequestHandler.java index 2aa074e..823f85c 100644 --- a/src/main/java/de/igslandstuhl/database/server/webserver/PostRequestHandler.java +++ b/src/main/java/de/igslandstuhl/database/server/webserver/handlers/PostRequestHandler.java @@ -1,4 +1,4 @@ -package de.igslandstuhl.database.server.webserver; +package de.igslandstuhl.database.server.webserver.handlers; import java.io.IOException; import java.io.UnsupportedEncodingException; @@ -31,11 +31,14 @@ import de.igslandstuhl.database.api.User; import de.igslandstuhl.database.api.results.GenerationResult; import de.igslandstuhl.database.server.Server; +import de.igslandstuhl.database.server.webserver.AccessLevel; +import de.igslandstuhl.database.server.webserver.ContentType; import de.igslandstuhl.database.server.webserver.requests.APIPostRequest; -import de.igslandstuhl.database.server.webserver.requests.HttpHandler; import de.igslandstuhl.database.server.webserver.requests.PostRequest; import de.igslandstuhl.database.server.webserver.responses.HttpResponse; import de.igslandstuhl.database.server.webserver.responses.PostResponse; +import de.igslandstuhl.database.server.webserver.sessions.Session; +import de.igslandstuhl.database.server.webserver.sessions.SessionManager; import de.igslandstuhl.database.utils.JSONUtils; import de.igslandstuhl.database.utils.ThrowingConsumer; @@ -399,5 +402,43 @@ public static void registerHandlers() { HttpHandler.registerPostRequestHandler("/change-graduation-level", AccessLevel.ADMIN, (rq) -> handleObjectAction(rq, new TypeToken() {}, 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); + }); } } \ No newline at end of file diff --git a/src/main/java/de/igslandstuhl/database/server/webserver/handlers/WebResourceHandler.java b/src/main/java/de/igslandstuhl/database/server/webserver/handlers/WebResourceHandler.java new file mode 100644 index 0000000..f4114f1 --- /dev/null +++ b/src/main/java/de/igslandstuhl/database/server/webserver/handlers/WebResourceHandler.java @@ -0,0 +1,72 @@ +package de.igslandstuhl.database.server.webserver.handlers; + +import de.igslandstuhl.database.Registry; +import de.igslandstuhl.database.api.User; +import de.igslandstuhl.database.server.resources.ResourceLocation; + +/** + * Handles the mapping of web resource paths to resource locations. + * This class provides methods to determine if a path is a SQL web resource, + * and to convert paths into ResourceLocation objects. + */ +public final class WebResourceHandler { + private WebResourceHandler(){} + + private static boolean isSQLWebResource(String path) { + return Registry.webPathRegistry().get(path) != null && + Registry.webPathRegistry().get(path).namespaces().contains("sql"); + } + + private static boolean inUserOnlySpace(String path) { + return Registry.webPathRegistry().get(path) != null && + Registry.webPathRegistry().get(path).namespaces().contains("user"); + } + private static boolean inTeacherOnlySpace(String path) { + return Registry.webPathRegistry().get(path) != null && + Registry.webPathRegistry().get(path).namespaces().contains("teacher"); + } + private static boolean inAdminOnlySpace(String path) { + return Registry.webPathRegistry().get(path) != null && + Registry.webPathRegistry().get(path).namespaces().contains("admin"); + } + private static String getDefaultNamespace(String path) { + return Registry.webPathRegistry().get(path) != null ? Registry.webPathRegistry().get(path).namespaces().get(0) : "site"; + } + + public static ResourceLocation locationFromPath(String path, User user) { + if (path.isEmpty()) path = "/"; + if (path == null || Registry.webPathRegistry().get(path) == null) { + return new ResourceLocation("site", "errors", "404.html"); + } + if (isSQLWebResource(path)) { + return new ResourceLocation("virtual", "sql", path.replaceFirst("/", "")); + } + + String context = Registry.webPathRegistry().get(path).context(); + + if (user == null) user = User.ANONYMOUS; + + String namespace; + if (inAdminOnlySpace(path) && (user == User.ANONYMOUS || user.isAdmin())) { + namespace = "admin"; + } else if (inTeacherOnlySpace(path) && (user == User.ANONYMOUS || user.isTeacher() || user.isAdmin())) { + namespace = "teacher"; + } else if (inUserOnlySpace(path)) { + namespace = "user"; + } else if (inAdminOnlySpace(path)) { + namespace = "admin"; + } else if (inTeacherOnlySpace(path)) { + namespace = "teacher"; + } else { + namespace = getDefaultNamespace(path); + } + String resource = path.replaceFirst("/", ""); + if (resource.isBlank()) { + resource = "index.html"; + } else if (!resource.contains(".")) { + resource += ".html"; + } + + return new ResourceLocation(context, namespace, resource); + } +} diff --git a/src/main/java/de/igslandstuhl/database/server/webserver/requests/APIPostRequest.java b/src/main/java/de/igslandstuhl/database/server/webserver/requests/APIPostRequest.java index dee8fdb..72edc41 100644 --- a/src/main/java/de/igslandstuhl/database/server/webserver/requests/APIPostRequest.java +++ b/src/main/java/de/igslandstuhl/database/server/webserver/requests/APIPostRequest.java @@ -17,7 +17,6 @@ import de.igslandstuhl.database.api.Topic; import de.igslandstuhl.database.api.User; import de.igslandstuhl.database.server.Server; -import de.igslandstuhl.database.server.webserver.HttpHeader; public class APIPostRequest extends PostRequest { public APIPostRequest(HttpHeader header, String body, String ipAddress, boolean secureConnection) { diff --git a/src/main/java/de/igslandstuhl/database/server/webserver/requests/GetRequest.java b/src/main/java/de/igslandstuhl/database/server/webserver/requests/GetRequest.java index bade7e1..ed5c256 100644 --- a/src/main/java/de/igslandstuhl/database/server/webserver/requests/GetRequest.java +++ b/src/main/java/de/igslandstuhl/database/server/webserver/requests/GetRequest.java @@ -7,8 +7,7 @@ import de.igslandstuhl.database.api.User; import de.igslandstuhl.database.server.resources.ResourceLocation; import de.igslandstuhl.database.server.webserver.Cookie; -import de.igslandstuhl.database.server.webserver.HttpHeader; -import de.igslandstuhl.database.server.webserver.WebResourceHandler; +import de.igslandstuhl.database.server.webserver.handlers.WebResourceHandler; /** * Represents a GET request in the web server. diff --git a/src/main/java/de/igslandstuhl/database/server/webserver/HttpHeader.java b/src/main/java/de/igslandstuhl/database/server/webserver/requests/HttpHeader.java similarity index 96% rename from src/main/java/de/igslandstuhl/database/server/webserver/HttpHeader.java rename to src/main/java/de/igslandstuhl/database/server/webserver/requests/HttpHeader.java index 658a93d..d08ade1 100644 --- a/src/main/java/de/igslandstuhl/database/server/webserver/HttpHeader.java +++ b/src/main/java/de/igslandstuhl/database/server/webserver/requests/HttpHeader.java @@ -1,8 +1,10 @@ -package de.igslandstuhl.database.server.webserver; +package de.igslandstuhl.database.server.webserver.requests; import java.util.ArrayList; import java.util.List; +import de.igslandstuhl.database.server.webserver.Cookie; + public class HttpHeader { /** * Represents the path of the POST request. diff --git a/src/main/java/de/igslandstuhl/database/server/webserver/requests/PostRequest.java b/src/main/java/de/igslandstuhl/database/server/webserver/requests/PostRequest.java index 5542af7..a0e0a40 100644 --- a/src/main/java/de/igslandstuhl/database/server/webserver/requests/PostRequest.java +++ b/src/main/java/de/igslandstuhl/database/server/webserver/requests/PostRequest.java @@ -11,7 +11,6 @@ import de.igslandstuhl.database.api.User; import de.igslandstuhl.database.server.resources.ResourceLocation; import de.igslandstuhl.database.server.webserver.Cookie; -import de.igslandstuhl.database.server.webserver.HttpHeader; /** * Represents a POST request in the web server. @@ -198,6 +197,6 @@ public boolean containsKey(String key) { * @return a ResourceLocation object representing the path of the POST request */ public ResourceLocation toResourceLocation(String username) { - return de.igslandstuhl.database.server.webserver.WebResourceHandler.locationFromPath(path, User.getUser(username)); + return de.igslandstuhl.database.server.webserver.handlers.WebResourceHandler.locationFromPath(path, User.getUser(username)); } } \ No newline at end of file diff --git a/src/main/java/de/igslandstuhl/database/server/webserver/requests/RequestType.java b/src/main/java/de/igslandstuhl/database/server/webserver/requests/RequestType.java new file mode 100644 index 0000000..a93f068 --- /dev/null +++ b/src/main/java/de/igslandstuhl/database/server/webserver/requests/RequestType.java @@ -0,0 +1,6 @@ +package de.igslandstuhl.database.server.webserver.requests; + +public enum RequestType { + GET, + POST +} diff --git a/src/main/java/de/igslandstuhl/database/server/webserver/responses/GetResponse.java b/src/main/java/de/igslandstuhl/database/server/webserver/responses/GetResponse.java index 0c3939a..188e038 100644 --- a/src/main/java/de/igslandstuhl/database/server/webserver/responses/GetResponse.java +++ b/src/main/java/de/igslandstuhl/database/server/webserver/responses/GetResponse.java @@ -22,28 +22,28 @@ public class GetResponse implements HttpResponse { * @return the GetResponse object */ public static GetResponse notFound(HttpRequest request) { - return new GetResponse(request, Status.NOT_FOUND, new ResourceLocation("html", "errors", "404.html"), ContentType.HTML, ""); + return new GetResponse(request, Status.NOT_FOUND, new ResourceLocation("html", "errors", "404.html"), ContentType.HTML, "", false); } /** * Returns a response for a GET request that resulted in an internal error. * @return the GetResponse object */ public static GetResponse internalServerError(HttpRequest request) { - return new GetResponse(request, Status.INTERNAL_SERVER_ERROR, new ResourceLocation("html", "errors", "500.html"), ContentType.HTML, ""); + return new GetResponse(request, Status.INTERNAL_SERVER_ERROR, new ResourceLocation("html", "errors", "500.html"), ContentType.HTML, "", false); } /** * Returns a response for a GET request the user has no access to. * @return the HttpRequest object */ public static GetResponse forbidden(HttpRequest request) { - return new GetResponse(request, Status.FORBIDDEN, new ResourceLocation("html", "errors", "403.html"), ContentType.HTML, ""); + return new GetResponse(request, Status.FORBIDDEN, new ResourceLocation("html", "errors", "403.html"), ContentType.HTML, "", false); } /** * Returns a response for a GET request the user must be logged in for. * @return the GetResponse object */ public static GetResponse unauthorized(HttpRequest request) { - return new GetResponse(request, Status.UNAUTHORIZED, new ResourceLocation("html", "errors", "401.html"), ContentType.HTML, ""); + return new GetResponse(request, Status.UNAUTHORIZED, new ResourceLocation("html", "errors", "401.html"), ContentType.HTML, "", false); } /** * The HTTP status of this response @@ -71,6 +71,8 @@ public static GetResponse unauthorized(HttpRequest request) { private final String user; private final HttpRequest request; + private final boolean isTemplating; + /** * Creates a new GetResponse with the given parameters. * This constructor is used to create a response for a GET request. @@ -78,13 +80,15 @@ public static GetResponse unauthorized(HttpRequest request) { * @param resourceLocation the location of the resource to be returned * @param contentType the HTTP content type of the resource * @param user the user who made the request + * @param isTemplating whether the resource is a templated resource */ - public GetResponse(HttpRequest request, Status status, ResourceLocation resourceLocation, ContentType contentType, String user) { + public GetResponse(HttpRequest request, Status status, ResourceLocation resourceLocation, ContentType contentType, String user, boolean isTemplating) { this.status = status; this.resourceLocation = resourceLocation; this.contentType = contentType; this.user = user; this.request = request; + this.isTemplating = isTemplating; } /** * Returns a response for a GET request for the given resource. @@ -92,10 +96,10 @@ public GetResponse(HttpRequest request, Status status, ResourceLocation resource * @param user the user who made the request * @return the GetResponse object */ - public static GetResponse getResource(HttpRequest request, ResourceLocation resourceLocation, String user) { + public static GetResponse getResource(HttpRequest request, ResourceLocation resourceLocation, String user, boolean isTemplating) { try { - if (AccessManager.hasAccess(user, resourceLocation)) { - return new GetResponse(request, Status.OK, resourceLocation, ContentType.ofResourceLocation(resourceLocation), user); + if (AccessManager.getInstance().hasAccess(user, resourceLocation)) { + return new GetResponse(request, Status.OK, resourceLocation, ContentType.ofResourceLocation(resourceLocation), user, isTemplating); } else { return unauthorized(request); } @@ -131,6 +135,9 @@ public void respond(PrintStream out) { if (resource == null) throw new NullPointerException(); } } + if (isTemplating) { + resource = TemplatingPreprocessor.getInstance().executeTemplating(resource); + } out.println(resource); } else { try (InputStream in = ResourceHelper.openResourceAsStream(resourceLocation)) { diff --git a/src/main/java/de/igslandstuhl/database/server/webserver/responses/PostResponse.java b/src/main/java/de/igslandstuhl/database/server/webserver/responses/PostResponse.java index 1fdde16..64b7044 100644 --- a/src/main/java/de/igslandstuhl/database/server/webserver/responses/PostResponse.java +++ b/src/main/java/de/igslandstuhl/database/server/webserver/responses/PostResponse.java @@ -155,8 +155,8 @@ public static PostResponse ok(String body, ContentType contentType, PostRequest */ public static PostResponse getResource(ResourceLocation resourceLocation, String user, PostRequest request) { try { - if (AccessManager.hasAccess(user, resourceLocation)) { - return new PostResponse(Status.OK, GetResponse.getResource(request, resourceLocation, user).getResponseBody(), ContentType.ofResourceLocation(resourceLocation), request); + if (AccessManager.getInstance().hasAccess(user, resourceLocation)) { + return new PostResponse(Status.OK, GetResponse.getResource(request, resourceLocation, user, false).getResponseBody(), ContentType.ofResourceLocation(resourceLocation), request); } else { return unauthorized("You have to be logged in to access this resource.", request); } diff --git a/src/main/java/de/igslandstuhl/database/server/webserver/responses/TemplatingPreprocessor.java b/src/main/java/de/igslandstuhl/database/server/webserver/responses/TemplatingPreprocessor.java new file mode 100644 index 0000000..7f144cb --- /dev/null +++ b/src/main/java/de/igslandstuhl/database/server/webserver/responses/TemplatingPreprocessor.java @@ -0,0 +1,126 @@ +package de.igslandstuhl.database.server.webserver.responses; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import de.igslandstuhl.database.server.resources.ResourceHelper; +import de.igslandstuhl.database.server.resources.ResourceLocation; + +public class TemplatingPreprocessor { + private static final TemplatingPreprocessor instance = new TemplatingPreprocessor(); + public static TemplatingPreprocessor getInstance() { + return instance; + } + + private TemplatingPreprocessor() {} + + private String getTemplate(String name) throws FileNotFoundException { + ResourceLocation templateLocation = new ResourceLocation("templates", "html", name + ".html"); + return ResourceHelper.readResourceCompletely(templateLocation); + } + + public String executeTemplating(String content) throws IOException { + if (content == null || content.isEmpty()) return content; + + StringBuilder out = new StringBuilder(); + int idx = 0; + while (idx < content.length()) { + int start = content.indexOf("%[", idx); + if (start < 0) { + // no more templates + out.append(content.substring(idx)); + break; + } + // append prefix + out.append(content.substring(idx, start)); + int end = content.indexOf("]", start + 2); + if (end < 0) { + throw new IOException("Unclosed template starting at " + start); + } + String inside = content.substring(start + 2, end).trim(); + if (inside.isEmpty()) { + idx = end + 1; + continue; + } + + // parse name and args + String[] parts = inside.split(";"); + String templateName = parts[0].trim(); + Map args = new HashMap<>(); + boolean usesFollows = false; + String followsKey = null; + + for (int i = 1; i < parts.length; i++) { + String part = parts[i]; + int eq = part.indexOf('='); + if (eq <= 0) continue; + String key = part.substring(0, eq).trim(); + String val = part.substring(eq + 1).trim(); + if ("!FOLLOWS".equals(val)) { + // the rest of the content is the value for this key + usesFollows = true; + followsKey = key; + break; + } else { + // expand templates inside argument values + String expandedVal = executeTemplating(val); + args.put(key, expandedVal); + } + } + + if (usesFollows) { + // the value is the rest of the content after the closing ']' + String rest = content.substring(end + 1); + // allow templates inside the follows value + String expandedRest = executeTemplating(rest); + args.put(followsKey, expandedRemapNull(expandedRest)); + // build template and finish (rest is consumed by this template) + String template = getTemplate(templateName); + String filled = fillTemplate(template, args); + // expand any templates produced by the filled template + String finalFilled = executeTemplating(filled); + out.append(finalFilled); + // consumed entire remaining content + idx = content.length(); + break; + } else { + // normal case: build template now, then continue after marker + String template = getTemplate(templateName); + String filled = fillTemplate(template, args); + // expand templates that might be present inside the filled template + String finalFilled = executeTemplating(filled); + out.append(finalFilled); + idx = end + 1; + } + } + + return out.toString(); + } + + // replace %{key} with args.getOrDefault(key, "") + private static String fillTemplate(String template, Map args) { + if (template == null || template.isEmpty()) return ""; + Pattern p = Pattern.compile("%\\{([^}]+)\\}"); + Matcher m = p.matcher(template); + StringBuffer sb = new StringBuffer(); + while (m.find()) { + String key = m.group(1); + String val = args.getOrDefault(key, ""); + // escape backslashes and dollars for regex replacement + val = val.replace("\\", "\\\\").replace("$", "\\$"); + m.appendReplacement(sb, val); + } + m.appendTail(sb); + return sb.toString(); + } + + // helper in case expanded rest is null + private static String expandedRemapNull(String s) { + return s == null ? "" : s; + } +} +// ...existing code... \ No newline at end of file diff --git a/src/main/java/de/igslandstuhl/database/server/webserver/Session.java b/src/main/java/de/igslandstuhl/database/server/webserver/sessions/Session.java similarity index 96% rename from src/main/java/de/igslandstuhl/database/server/webserver/Session.java rename to src/main/java/de/igslandstuhl/database/server/webserver/sessions/Session.java index a64cf6d..84a493a 100644 --- a/src/main/java/de/igslandstuhl/database/server/webserver/Session.java +++ b/src/main/java/de/igslandstuhl/database/server/webserver/sessions/Session.java @@ -1,10 +1,11 @@ -package de.igslandstuhl.database.server.webserver; +package de.igslandstuhl.database.server.webserver.sessions; import java.time.Instant; import java.util.Locale; import java.util.Objects; import java.util.UUID; +import de.igslandstuhl.database.server.webserver.Cookie; import de.igslandstuhl.database.server.webserver.requests.HttpRequest; public class Session { diff --git a/src/main/java/de/igslandstuhl/database/server/webserver/SessionManager.java b/src/main/java/de/igslandstuhl/database/server/webserver/sessions/SessionManager.java similarity index 97% rename from src/main/java/de/igslandstuhl/database/server/webserver/SessionManager.java rename to src/main/java/de/igslandstuhl/database/server/webserver/sessions/SessionManager.java index 72d212e..01a2262 100644 --- a/src/main/java/de/igslandstuhl/database/server/webserver/SessionManager.java +++ b/src/main/java/de/igslandstuhl/database/server/webserver/sessions/SessionManager.java @@ -1,4 +1,4 @@ -package de.igslandstuhl.database.server.webserver; +package de.igslandstuhl.database.server.webserver.sessions; import java.time.Instant; import java.util.HashMap; @@ -6,6 +6,7 @@ import java.util.UUID; import de.igslandstuhl.database.api.User; +import de.igslandstuhl.database.server.webserver.Cookie; import de.igslandstuhl.database.server.webserver.requests.HttpRequest; public class SessionManager { diff --git a/src/main/java/de/igslandstuhl/database/server/webserver/SessionStorage.java b/src/main/java/de/igslandstuhl/database/server/webserver/sessions/SessionStorage.java similarity index 95% rename from src/main/java/de/igslandstuhl/database/server/webserver/SessionStorage.java rename to src/main/java/de/igslandstuhl/database/server/webserver/sessions/SessionStorage.java index 8780a9e..5d8feca 100644 --- a/src/main/java/de/igslandstuhl/database/server/webserver/SessionStorage.java +++ b/src/main/java/de/igslandstuhl/database/server/webserver/sessions/SessionStorage.java @@ -1,4 +1,4 @@ -package de.igslandstuhl.database.server.webserver; +package de.igslandstuhl.database.server.webserver.sessions; import java.util.HashMap; import java.util.Map; diff --git a/src/main/java/de/igslandstuhl/database/utils/CommandLineUtils.java b/src/main/java/de/igslandstuhl/database/utils/CommandLineUtils.java index fa0fae2..8f43174 100644 --- a/src/main/java/de/igslandstuhl/database/utils/CommandLineUtils.java +++ b/src/main/java/de/igslandstuhl/database/utils/CommandLineUtils.java @@ -1,45 +1,39 @@ package de.igslandstuhl.database.utils; -import java.io.PrintStream; import java.util.Arrays; -import java.util.Scanner; +import org.jline.reader.*; +import org.jline.reader.impl.completer.StringsCompleter; +import org.jline.terminal.Terminal; +import org.jline.terminal.TerminalBuilder; import de.igslandstuhl.database.server.commands.Command; public class CommandLineUtils { - private static boolean waitingForInput = false; - private static String prompt = ""; - private static PrintStream origin = System.out; - private static final Scanner SCANNER = new Scanner(System.in); - public static synchronized String input(Object... messages) { - waitingForInput = true; - prompt = Arrays.stream(messages).map(String::valueOf).map((s) -> s + " ").reduce("", (s1,s2) -> s1+s2) + ">>>"; + private static LineReader reader; + private static Terminal terminal; + + public static void setup() { try { - printPrompt(origin); - return SCANNER.nextLine(); - } finally { - waitingForInput = false; + terminal = TerminalBuilder.terminal(); + // Provide all available commands for auto-completion + reader = LineReaderBuilder.builder() + .terminal(terminal) + .completer(new StringsCompleter(Command.getAllCommandNames())) + .build(); + } catch (Exception e) { + throw new RuntimeException("Failed to initialize terminal", e); } } + + public static synchronized String input(Object... messages) { + String prompt = Arrays.stream(messages).map(String::valueOf).reduce("", (s1,s2) -> s1 + s2 + " ") + ">>> "; + return reader.readLine(prompt); + } + public static void waitForCommandAndExec() { - String[] args = input().split(" "); + String line = input(); + String[] args = line.trim().split(" "); String command = args[0]; System.out.println(Command.executeCommand(command, Arrays.copyOfRange(args, 1, args.length))); } - private static void printPrompt(PrintStream stream) { - if (waitingForInput){ - stream.print(prompt); - } - } - private static void clearPrompt(PrintStream stream) { - if (waitingForInput) { - stream.print("\u001B[2K\r"); - stream.flush(); - } - } - public static void setup() { - origin = System.out; - System.setOut(new PrintStreamWrapper(origin, origin, CommandLineUtils::clearPrompt, CommandLineUtils::printPrompt)); - System.setErr(new PrintStreamWrapper(System.err, origin, CommandLineUtils::clearPrompt, CommandLineUtils::printPrompt)); - } } diff --git a/src/main/java/de/igslandstuhl/database/utils/StringUtils.java b/src/main/java/de/igslandstuhl/database/utils/StringUtils.java new file mode 100644 index 0000000..fce6289 --- /dev/null +++ b/src/main/java/de/igslandstuhl/database/utils/StringUtils.java @@ -0,0 +1,12 @@ +package de.igslandstuhl.database.utils; + +public class StringUtils { + private StringUtils() {} + public static String fitSize(String str, int size) { + if (str.length() > size) { + return str.substring(0, size-3) + "..."; + } else { + return str + " ".repeat(size - str.length()); + } + } +} diff --git a/src/main/resources/css/site/results.css b/src/main/resources/css/site/results.css new file mode 100644 index 0000000..2c9de93 --- /dev/null +++ b/src/main/resources/css/site/results.css @@ -0,0 +1,35 @@ +.bar-chart { margin-bottom: 40px; } +.bar-container { display: flex; align-items: flex-end; height: 60px; margin-top: 10px; } +.bar-task { + height: 40px; + margin-right: 2px; + border-radius: 4px 4px 0 0; + border: 1px solid #ccc; + position: relative; + box-sizing: border-box; +} +.bar-task.level1 { background: #f5e9d4; } /* beige */ +.bar-task.level2 { background: #b3d1ff; } /* blue */ +.bar-task.level3 { background: #fff9b3; } /* yellow */ +.bar-task.special { background: #ffdddd;} /* pink */ +.bar-task.hatched { + background-image: repeating-linear-gradient( + 45deg, + rgba(0,0,0,0.1) 0, rgba(0,0,0,0.1) 4px, + transparent 4px, transparent 8px + ); +} +.bar-task.completed { opacity: 1; } +.bar-task.selected:not(.completed) { opacity: 0.7; } +.bar-task:not(.completed):not(.selected) { opacity: 0.3; } +.grade-scale { display: flex; justify-content: space-between; margin-bottom: 4px; font-size: 0.9em; } +.grade-label { font-weight: bold; } +.grade-current, .grade-prediction { margin-top: 8px; } +.bar-task-label { + position: absolute; + bottom: 100%; left: 50%; + transform: translateX(-50%); + font-size: 0.8em; + white-space: nowrap; + margin-bottom: 2px; +} \ No newline at end of file diff --git a/src/main/resources/css/site/style.css b/src/main/resources/css/site/style.css index 95c3f79..6049629 100644 --- a/src/main/resources/css/site/style.css +++ b/src/main/resources/css/site/style.css @@ -28,26 +28,26 @@ select { margin-left: 10px; } -.subject-panel { +.panel { border: 1px solid #ccc; border-radius: 5px; margin-bottom: 10px; background: #f8f9fa; } -.subject-panel h3 { +.panel h3 { margin: 0; padding: 10px; cursor: pointer; background: #dee2e6; } -.subject-panel .panel-body { +.panel .panel-body { display: none; padding: 10px; } -.subject-panel.active .panel-body { +.panel.active .panel-body { display: block; } diff --git a/src/main/resources/html/admin/class.html b/src/main/resources/html/admin/class.html index 76506fc..af1c639 100644 --- a/src/main/resources/html/admin/class.html +++ b/src/main/resources/html/admin/class.html @@ -1,69 +1,58 @@ - - - - - - Klasse bearbeiten - - - - -
-
-

Klasse bearbeiten

-
-
-
-

Daten bearbeiten

-
- - - - - - -
-
-
-

Klasse löschen

- -
-
-

Fächer der Klasse

-
    - -
-
-

Fach hinzufügen

-
- - - - -
-
-
-
-

Schüler in der Klasse

- - - - - - - - - - - - -
NameRaumGraduierung
-
-
- +%[site;title=Klasse bearbeiten;content=!FOLLOWS] +
+
+

Klasse bearbeiten

+
+
+
+

Daten bearbeiten

+
+ + + + + + +
- - \ No newline at end of file +
+

Klasse löschen

+ +
+
+

Fächer der Klasse

+
    + +
+
+

Fach hinzufügen

+
+ + + + +
+
+
+
+

Schüler in der Klasse

+ + + + + + + + + + + + +
NameRaumGraduierung
+
+
+ +
\ No newline at end of file diff --git a/src/main/resources/html/admin/dashboard.html b/src/main/resources/html/admin/dashboard.html index b3de792..0087ca7 100644 --- a/src/main/resources/html/admin/dashboard.html +++ b/src/main/resources/html/admin/dashboard.html @@ -1,60 +1,19 @@ - - - - - - Dashboard - - - -
-
-

Admin Dashboard

-
-

Willkommen im Admin-Dashboard!

-
-

Admin Optionen

- -
-
-

Datenimport aus Lehrerplanungstool

-

Hinweis: Stellen Sie sicher, dass das jeweilige Fach bereits existiert, und der jeweiligen Klassenstufe zugeordnet ist.

-
- - -
- -
+%[site;title=Dashboard;content=!FOLLOWS] +
+
+

Admin Dashboard

+
+

Willkommen im Admin-Dashboard!

+
+

Admin Optionen

+
- - \ No newline at end of file + %[file_import;header=Datenimport aus Lehrerplanungstool;note=Hinweis: Stellen Sie sicher, dass das jeweilige Fach bereits existiert, und der jeweiligen Klassenstufe zugeordnet ist.;uploadUrl=/lpt-file] +
\ No newline at end of file diff --git a/src/main/resources/html/admin/manage_classes.html b/src/main/resources/html/admin/manage_classes.html index 037114f..83df6c8 100644 --- a/src/main/resources/html/admin/manage_classes.html +++ b/src/main/resources/html/admin/manage_classes.html @@ -1,73 +1,53 @@ - - - - - - Klassen verwalten - - - -
-
-

Klassen verwalten

-
-
-
-

Klasse hinzufügen

-
- - - - - -
-
-
-

Klassenübersicht

- - - - - - - - - - - - -
IDNameStufe
-
-
- +%[site;title=Klassen verwalten;content=!FOLLOWS] +
+
+

Klassen verwalten

+
+
+
+

Klasse hinzufügen

+
+ + + + + +
- - - \ No newline at end of file + async function loadClasses() { + populateTable("/classes", "classTable", (newRow, cls) => { + newRow.innerHTML = ` + ${cls.id} + ${cls.label} + ${cls.grade} + + `; + }); + } + \ No newline at end of file diff --git a/src/main/resources/html/admin/manage_rooms.html b/src/main/resources/html/admin/manage_rooms.html index e368455..ac8a526 100644 --- a/src/main/resources/html/admin/manage_rooms.html +++ b/src/main/resources/html/admin/manage_rooms.html @@ -1,93 +1,46 @@ - - - - - - Raumverwaltung - - - -
-
-

Raumverwaltung

-
-
-
-

Räume hinzufügen

-
- - - -
-
-
-

Raumliste

- - - - - - - - - - -
RaumnameMindestlevel
-
-
- +%[site;title=Raumverwaltung;content=!FOLLOWS] +
+
+

Raumverwaltung

+
+
+
+

Räume hinzufügen

+
+ + + +
- - \ No newline at end of file +
+

Raumliste

+ + + + + + + + + + +
RaumnameMindestlevel
+
+
+ +
+ \ No newline at end of file diff --git a/src/main/resources/html/admin/manage_students.html b/src/main/resources/html/admin/manage_students.html index 8622f08..374bdaa 100644 --- a/src/main/resources/html/admin/manage_students.html +++ b/src/main/resources/html/admin/manage_students.html @@ -1,167 +1,102 @@ - - - - - - Schülerverwaltung - - - -
-
-

Schülerverwaltung

-
-
-
-

Schüler hinzufügen aus edu.sys

-
- - - -
-
-
-

Schülerliste

- -
- - -
- - - - - - - - - - -
NameRaumGraduierung
-
-
- +%[site;title=Schülerverwaltung;content=!FOLLOWS] +
+
+

Schülerverwaltung

+
+
+
+

Schüler hinzufügen aus edu.sys

+
+ + + +
- - - \ No newline at end of file + newRow.getElementsByClassName('student-graduation-level')[0].appendChild(graduationLevelSelect); + }); + } + + document.addEventListener('DOMContentLoaded', async () => { + const classSelect = document.getElementById('classSelect'); + const classes = await fetchClasses(); + populateClassSelect(classSelect, classes); + classSelect.addEventListener('change', onClassChange); + onClassChange({ target: classSelect }); // Trigger initial load + populateSubjectSelect('subjectSelect', await fetchAllSubjects()); + }); + \ No newline at end of file diff --git a/src/main/resources/html/admin/manage_subjects.html b/src/main/resources/html/admin/manage_subjects.html index 271e366..896ca08 100644 --- a/src/main/resources/html/admin/manage_subjects.html +++ b/src/main/resources/html/admin/manage_subjects.html @@ -1,55 +1,40 @@ - - - - - - Fächer verwalten - - - -
-
-

Fächer verwalten

-
-
-
-
- - - -
-
-
-

Vorhandene Fächer

-
    - -
-
-
- +%[site;title=Fächer verwalten;content=!FOLLOWS] +
+
+

Fächer verwalten

+
+
+
+
+ + + +
- - - \ No newline at end of file +
+

Vorhandene Fächer

+
    + +
+
+
+ +
+ \ No newline at end of file diff --git a/src/main/resources/html/admin/manage_teachers.html b/src/main/resources/html/admin/manage_teachers.html index b3c0f7f..2a53d67 100644 --- a/src/main/resources/html/admin/manage_teachers.html +++ b/src/main/resources/html/admin/manage_teachers.html @@ -1,123 +1,68 @@ - - - - - - Lehrer verwalten - - - -
-
-

Lehrer verwalten

-
-
-
-

Lehrer hinzufügen

-
- - - - - - - -
-
-
-

Lehrer aus CSV hinzufügen

-
- - - -
-
-
-

Lehrer-Liste

- - - - - - - - - - - - - -
VornameNachnameEmailAktionen
-
-
- -
- - - \ No newline at end of file + }); + \ No newline at end of file diff --git a/src/main/resources/html/admin/modules.html b/src/main/resources/html/admin/modules.html new file mode 100644 index 0000000..01f6d23 --- /dev/null +++ b/src/main/resources/html/admin/modules.html @@ -0,0 +1,7 @@ +%[site;title=Lehrer verwalten;content=!FOLLOWS] +
+
+

Module

+
+

Willkommen in der Modulübersicht!

+
\ No newline at end of file diff --git a/src/main/resources/html/admin/student-results.html b/src/main/resources/html/admin/student-results.html index aad7527..d295208 100644 --- a/src/main/resources/html/admin/student-results.html +++ b/src/main/resources/html/admin/student-results.html @@ -1,59 +1,22 @@ - - - - - - Ergebnisse – Lernsystem - - - - -
-
-

Ergebnisse für

-
-
- -
- - - \ No newline at end of file +%[student_results;nav= + + + + +
+ + +
+ +] \ No newline at end of file diff --git a/src/main/resources/html/admin/student.html b/src/main/resources/html/admin/student.html index bdc5c59..2ea49f3 100644 --- a/src/main/resources/html/admin/student.html +++ b/src/main/resources/html/admin/student.html @@ -1,32 +1,6 @@ - - - - - - Schüler-Dashboard - - - -
-
-

Schüler:

-

Klasse: | E-Mail: | Graduierung:

-
-
- - -
-
-

Fächer

-
-
- -
- - - \ No newline at end of file +%[student_dashboard;script=build_student.js;nav= + + + + +] \ No newline at end of file diff --git a/src/main/resources/html/admin/subject.html b/src/main/resources/html/admin/subject.html index c70d201..faea4b5 100644 --- a/src/main/resources/html/admin/subject.html +++ b/src/main/resources/html/admin/subject.html @@ -1,66 +1,56 @@ - - - - - - Fach bearbeiten - - - - -
-
-

Fach bearbeiten

-
-
-

Daten bearbeiten

-
- - - - -
-
-
-

Fach löschen

- -
-
-

Klassenstufen

-
    - -
-
-

Klassenstufe hinzufügen

-
- - - - -
-
-
-
-

Themen in diesem Fach

- - - - - - - - - - - - -
NameAnteilNummerEtappen gesamt
- -
- +%[site;title=Fach verwalten;content=!FOLLOWS] + +
+
+

Fach bearbeiten

+
+
+

Daten bearbeiten

+
+ + + + +
+
+
+

Fach löschen

+ +
+
+

Klassenstufen

+
    + +
+
+

Klassenstufe hinzufügen

+
+ + + + +
- - \ No newline at end of file +
+
+

Themen in diesem Fach

+ + + + + + + + + + + + +
NameAnteilNummerEtappen gesamt
+ +
+ +
\ No newline at end of file diff --git a/src/main/resources/html/admin/teacher.html b/src/main/resources/html/admin/teacher.html index 8373c21..d36a908 100644 --- a/src/main/resources/html/admin/teacher.html +++ b/src/main/resources/html/admin/teacher.html @@ -1,112 +1,31 @@ - - - - - - Lehrerverwaltung - - - -
-
-

Lehrerverwaltung -

-
-
-
-

Lehrer bearbeiten

-
- - - - -
-
- - - - -
-
-
-

Klassenübersicht

-
- - -
-
-

Schüler in der Klasse:

- - - - - - - - - - -
NameRaumGraduierung
-
-
-
-

Fächerübersicht

-
- - - - -
-
-

Schüler in der Klasse:

- - - - - - - - - - - - -
NameBraucht UnterstützungBraucht Betreuung für ExperimentSucht PartnerIst bereit für den Gelingensnachweis
-
- -
-

Raumübersicht

-
- - -
-
-

Schüler im Raum:

- - - - - - - - - -
NameBraucht Unterstützung
-
-
-
-
- +%[site;title=Lehrerverwaltung;content=!FOLLOWS] +
+
+

Lehrerverwaltung -

+
+
+
+

Lehrer bearbeiten

+
+ + + + +
+
+ + + + +
- - - - \ No newline at end of file + %[class_info] + %[subject_info] + %[room_info] +
+ +
+ \ No newline at end of file diff --git a/src/main/resources/html/teacher/dashboard.html b/src/main/resources/html/teacher/dashboard.html index 1c66257..f53faab 100644 --- a/src/main/resources/html/teacher/dashboard.html +++ b/src/main/resources/html/teacher/dashboard.html @@ -1,82 +1,9 @@ - - - - - - Lehrer Dashboard - - - - -
-
-

Willkommen, Lehrer

-
-
-

Klassenübersicht

-
- - -
-
-

Schüler in der Klasse:

- - - - - - - - - - -
NameRaumGraduierung
-
-
-
-

Fächerübersicht

-
- - - - -
-
-

Schüler in der Klasse:

- - - - - - - - - - - - -
NameBraucht UnterstützungBraucht Betreuung für ExperimentSucht PartnerIst bereit für den Gelingensnachweis
-
-
-

Raumübersicht

-
- - -
-
-

Schüler im Raum:

- - - - - - - - - -
NameBraucht Unterstützung
-
-
-
- - \ No newline at end of file +%[site;title=Lehrer-Dashboard;content=!FOLLOWS] +
+
+

Willkommen, Lehrer

+
+ %[class_info] + %[subject_info] + %[room_info] +
\ No newline at end of file diff --git a/src/main/resources/html/teacher/student-results.html b/src/main/resources/html/teacher/student-results.html index 9c06cc0..3ffafe5 100644 --- a/src/main/resources/html/teacher/student-results.html +++ b/src/main/resources/html/teacher/student-results.html @@ -1,107 +1,8 @@ - - - - - - Ergebnisse – Lernsystem - - - - -
-
-

Ergebnisse für

-
-
- -
- - - \ No newline at end of file +%[student_results;nav= + + +
+ + +
+] \ No newline at end of file diff --git a/src/main/resources/html/teacher/student.html b/src/main/resources/html/teacher/student.html index d275b7f..7b597a9 100644 --- a/src/main/resources/html/teacher/student.html +++ b/src/main/resources/html/teacher/student.html @@ -1,31 +1,4 @@ - - - - - - Schüler-Dashboard - - - -
-
-

Schüler:

-

Klasse: | E-Mail: | Graduierung:

-
-
- - -
-
-

Fächer

-
-
- -
- - - \ No newline at end of file +%[student_dashboard;script=build_student.js;nav= + + +] \ No newline at end of file diff --git a/src/main/resources/html/user/dashboard.html b/src/main/resources/html/user/dashboard.html index 0b009b1..8b20dad 100644 --- a/src/main/resources/html/user/dashboard.html +++ b/src/main/resources/html/user/dashboard.html @@ -1,34 +1,4 @@ - - - - - - Dashboard – Lernsystem - - - -
-
-

Willkommen,

-

Klasse: | E-Mail: | Graduierung:

-
- -
- - -
- -
-

Fächer

-
-
- - -
- - - - +%[student_dashboard;script=build_dashboard.js;nav= + + +] \ No newline at end of file diff --git a/src/main/resources/html/user/partner_search.html b/src/main/resources/html/user/partner_search.html index 264c45d..0c36837 100644 --- a/src/main/resources/html/user/partner_search.html +++ b/src/main/resources/html/user/partner_search.html @@ -1,32 +1,22 @@ - - - - - - Partnersuche - - - -
-
-

Partnersuche

-
-
- - - - - - - - - -
NameRaum
-
- - -
- - \ No newline at end of file +%[site;title=Partnersuche;content=!FOLLOWS] +
+
+

Partnersuche

+
+
+ + + + + + + + + +
NameRaum
+
+ + +
\ No newline at end of file diff --git a/src/main/resources/html/user/results.html b/src/main/resources/html/user/results.html index fdbbd75..d123147 100644 --- a/src/main/resources/html/user/results.html +++ b/src/main/resources/html/user/results.html @@ -1,106 +1,3 @@ - - - - - - Ergebnisse – Lernsystem - - - - -
-
-

Ergebnisse für

-
-
- -
- - - \ No newline at end of file +%[student_results;nav= + +] \ No newline at end of file diff --git a/src/main/resources/js/admin/build_class.js b/src/main/resources/js/admin/build_class.js index 7717b00..bb6a271 100644 --- a/src/main/resources/js/admin/build_class.js +++ b/src/main/resources/js/admin/build_class.js @@ -1,47 +1,5 @@ cls = JSON.parse(sessionStorage.getItem('currentClass')); -async function fetchJson(url, options) { - const res = await fetch(url, options); - return await res.json(); -} - -function populateStudentTable(studentTable, students) { - studentTable.innerHTML = ''; // Clear existing rows - const graduationLevels = ["Neustarter", "Starter", "Durchstarter", "Lernprofi"]; - students.forEach(function(student) { - const row = document.createElement('tr'); - row.innerHTML = ` - ${student.name} - ${student.room} - ${graduationLevels[student.graduationLevel]} - - `; - studentTable.appendChild(row); - }); -} -function populateSubjectList(subjectList, subjects) { - subjectList.innerHTML = ''; // Clear existing items - subjects.forEach(function(subject) { - const listItem = document.createElement('li'); - listItem.textContent = subject.name; - subjectList.appendChild(listItem); - }); -} -function populateSubjectSelect(subjectSelect, subjects) { - subjectSelect.innerHTML = ''; // Clear existing options - subjects.forEach(function(subject) { - const option = document.createElement('option'); - option.value = subject.id; - option.textContent = subject.name; - subjectSelect.appendChild(option); - }); -} - -function viewStudent(student) { - sessionStorage.setItem('currentStudent', JSON.stringify(student)); - window.location.href = '/student'; -} - document.addEventListener('DOMContentLoaded', async function() { if (cls) { document.getElementById('className').value = cls.label; @@ -55,14 +13,7 @@ document.addEventListener('DOMContentLoaded', async function() { } document.getElementById('deleteClassButton').addEventListener('click', function() { if (confirm('Are you sure you want to delete this class?')) { - fetch('/delete-class', { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ id: cls.id }) - }) - .then(response => { + deleteClass(cls.id).then(response => { if (response.ok) { alert('Class deleted successfully.'); window.location.href = '/manage_classes'; @@ -76,28 +27,17 @@ document.addEventListener('DOMContentLoaded', async function() { }); } }); - - const studentTableBody = document.querySelector('#studentTable tbody'); - students = await fetchJson("/student-list", { - method: 'POST', - body: JSON.stringify({ classId: Number(cls.id) }), - headers: { - 'Content-Type': 'application/json' - } + + await populateStudentTable(Number(cls.id), "studentTable", (row, student) => { + row.innerHTML = ` + ${student.name} + ${student.room} + ${graduationLevels[student.graduationLevel]} + + `; }); - populateStudentTable(studentTableBody, students); - const subjectList = document.getElementById('subjectList'); - const subjects = await fetchJson("/class-subjects", { - method: 'POST', - body: JSON.stringify({ classId: Number(cls.id) }), - headers: { - 'Content-Type': 'application/json' - } - }); - populateSubjectList(subjectList, subjects); + await populateSubjectList('subjectList', Number(cls.id)); - const subjectSelect = document.getElementById('subjectSelect'); - const allSubjects = await fetchJson('/subjects') - populateSubjectSelect(subjectSelect, allSubjects); + await populateSubjectSelect('subjectSelect'); }); \ No newline at end of file diff --git a/src/main/resources/js/admin/build_modules.js b/src/main/resources/js/admin/build_modules.js new file mode 100644 index 0000000..e69de29 diff --git a/src/main/resources/js/admin/build_subject.js b/src/main/resources/js/admin/build_subject.js index 85700b8..6d9542d 100644 --- a/src/main/resources/js/admin/build_subject.js +++ b/src/main/resources/js/admin/build_subject.js @@ -1,74 +1,5 @@ subject = JSON.parse(sessionStorage.getItem("currentSubject")) -async function fetchJson(url, options) { - const res = await fetch(url, options); - return await res.json(); -} - -function openUrlWithPostParams(url, params) { - const form = document.createElement("form"); - form.setAttribute("method", "post"); - form.setAttribute("action", url); - - Object.keys(params).forEach((key) => { - const input = document.createElement("input"); - input.setAttribute("type", "hidden") - input.setAttribute("name", key) - input.setAttribute("value", params[key]) - form.appendChild(input) - }) - - const submitButton = document.createElement("button") - submitButton.setAttribute("type", "submit") - - form.appendChild(submitButton) - document.getElementsByTagName("body")[0].appendChild(form) - - submitButton.click() -} - -function populateGradeList(list, grades) { - list.innerHTML = ''; - grades.forEach(grade => { - const li = document.createElement("li"); - li.textContent = grade; - li.style.cursor = 'pointer'; - li.title = 'Klicken, um die Klasenstufe zu entfernen'; - li.addEventListener('click', (e) => { - if (confirm('Soll die Klassenstufe wirklich von diesem Fach entfernt werden?')) { - openUrlWithPostParams('/delete-grade-from-subject', { - "subject": subject.id, - "grade": grade - }) - } - }) - list.appendChild(li) - }); -} -function populateGradeSelect(gradeSelect, grades) { - gradeSelect.innerHTML = ''; - grades.forEach(grade => { - const option = document.createElement('option'); - option.text = grade; - option.value = grade; - gradeSelect.appendChild(option) - }) -} - -function populateTopicTable(table, topics) { - table.innerHTML = ''; - topics.forEach(topic => { - const row = document.createElement('tr'); - row.innerHTML = ` - ${topic.name} - ${topic.ratio} - ${topic.number} - ${topic.tasks.length} - ` - table.appendChild(row) - }) -} - document.addEventListener('DOMContentLoaded', async (e) => { if (subject) { document.getElementById('subjectNameField').value = subject.name; @@ -81,14 +12,7 @@ document.addEventListener('DOMContentLoaded', async (e) => { document.getElementById('deleteSubjectButton').addEventListener('click', function() { if (confirm('Are you sure you want to delete this subject?')) { - fetch('/delete-subject', { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ id: subject.id }) - }) - .then(response => { + deleteSubject(subject.id).then(response => { if (response.ok) { alert('Subject deleted successfully.'); window.location.href = '/manage_subjects'; @@ -103,34 +27,11 @@ document.addEventListener('DOMContentLoaded', async (e) => { } }); - const gradeList = document.getElementById('gradeList') - const gradeSelect = document.getElementById('gradeSelect') - const grades = await fetchJson('/grade-list', { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ - subjectId: subject.id - }) - }) - populateGradeList(gradeList, grades); - populateGradeSelect(gradeSelect, grades); + await populateGradeList('gradeList', subject.id); + await populateGradeSelect('gradeSelect', subject.id); async function updateTopicTable(e) { - const grade = Number(e.target.value); - console.log(document.getElementById("topicTable")) - const topicTable = document.querySelector("#topicTable tbody"); - const topics = await fetchJson('/topic-list', { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ - subjectId: subject.id, grade - }) - }) - populateTopicTable(topicTable, topics); + await populateTopicTable('topicTable', subject.id, Number(e.target.value)); } gradeSelect.addEventListener('change', updateTopicTable); diff --git a/src/main/resources/js/admin/build_teacher.js b/src/main/resources/js/admin/build_teacher.js index 0fedd0c..de18d17 100644 --- a/src/main/resources/js/admin/build_teacher.js +++ b/src/main/resources/js/admin/build_teacher.js @@ -1,173 +1,13 @@ -async function fetchJson(url, options) { - const res = await fetch(url, options); - if (res.ok) { - return await res.json(); - } else { - return [] - } -} - -async function fetchClasses() { - const classes = await fetchJson('/teacher-classes', { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ teacherId }) - }); - return classes; -} -async function fetchSubjects() { - const subjects = await fetchJson('/teacher-subjects', { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ teacherId }) - }); - return subjects; -} -function populateRoomSelect(roomSelect, rooms) { - roomSelect.innerHTML = ""; // clear previous options if any - rooms.forEach(room => { - const option = document.createElement('option'); - option.value = room.label; - option.textContent = room.label; - roomSelect.appendChild(option); - }); -} - -function populateClassSelect(classSelect, classes) { - classSelect.innerHTML = ""; // clear previous options if any - classes.forEach(cls => { - const option = document.createElement('option'); - option.value = cls.classId || cls.id; - option.textContent = cls.name || cls.label; - classSelect.appendChild(option); - }); -} -function populateSubjectSelect(subjectSelect, subjects) { - console.log("Populating subject select with subjects:", subjects); - subjectSelect.innerHTML = ""; // clear previous options if any - subjects.forEach(subject => { - const option = document.createElement('option'); - option.value = subject.subjectId || subject.id; // Use subjectId or id based on your API - option.textContent = subject.name; - subjectSelect.appendChild(option); - }); -} - -async function onClassChange(event) { - const selectedClassId = event.target.value; - const students = await fetchJson("/student-list", { - method: 'POST', - body: JSON.stringify({ classId: Number(selectedClassId) }), - headers: { - 'Content-Type': 'application/json' - } - }); - const studentTable = document.getElementById("studentTableBody"); - studentTable.innerHTML = ""; // clear previous rows - students.forEach(student => { - const row = document.createElement('tr'); - const graduationLevels = ["Neustarter", "Starter", "Durchstarter", "Lernprofi"]; - row.innerHTML = ` - ${student.name} - ${student.room} - ${graduationLevels[student.graduationLevel]} - - `; - studentTable.appendChild(row); - }); -} -async function populateSubjectStudentList(event) { - const subjectSelect = document.getElementById('subjectSelect'); - const classSelect = document.getElementById('classSelectSubject'); - const selectedClassId = classSelect.value; - - if (!selectedClassId) { - subjectSelect.innerHTML = ""; // clear previous options if no class is selected - return; - } - - const students = await fetchJson("/student-list", { - method: 'POST', - body: JSON.stringify({ classId: Number(selectedClassId), subjectId: Number(subjectSelect.value), teacherId }), - headers: { - 'Content-Type': 'application/json' - } - }); - - const studentTable = document.getElementById("subjectStudentTableBody"); - studentTable.innerHTML = ""; // clear previous rows - students.forEach(student => { - const row = document.createElement('tr'); - row.innerHTML = ` - ${student.name} - ${student.help ? "Ja" : "Nein"} - ${student.experiment ? "Ja" : "Nein"} - ${student.partner ? "Ja" : "Nein"} - ${student.test ? "Ja" : "Nein"} - - `; - studentTable.appendChild(row); - }); -} -async function populateRoomStudentList(event) { - const room = event.target.value; - - const students = await fetchJson("/get-students-by-room", { - method: 'POST', - body: JSON.stringify({ room }), - headers: { - 'Content-Type': 'application/json' - } - }); - - const studentTable = document.getElementById("roomStudentTableBody"); - studentTable.innerHTML = ""; // clear previous rows - students.forEach(student => { - const row = document.createElement('tr'); - row.innerHTML = ` - ${student.name} - ${student.actionRequired ? "Ja" : "Nein"} - - `; - studentTable.appendChild(row); - }); -} -function viewStudent(studentId) { - // Add studentId to session storage - sessionStorage.setItem('selectedStudentId', studentId); - // Redirect to student dashboard - window.location.href = `/student`; -} +let teacher = JSON.parse(sessionStorage.getItem("currentTeacher")); +console.log(teacher); +document.getElementById("teacher-name").textContent = teacher.firstName + " " + teacher.lastName; +let teacherId = teacher.id; +Array.from(document.getElementsByClassName("teacherId")).forEach(e => e.value = teacherId); document.addEventListener('DOMContentLoaded', async () => { - const classSelect = document.getElementById('classSelect'); - const subjectClassSelect = document.getElementById('classSelectSubject'); - const classes = await fetchClasses(); - populateClassSelect(classSelect, classes); - populateClassSelect(subjectClassSelect, classes); - classSelect.addEventListener('change', onClassChange); - subjectClassSelect.addEventListener('change', populateSubjectStudentList); - onClassChange({ target: classSelect }); // Trigger initial load - const subjectSelect = document.getElementById('subjectSelect'); - const subjects = await fetchSubjects(); - populateSubjectSelect(subjectSelect, subjects); - subjectSelect.addEventListener('change', populateSubjectStudentList); - populateSubjectStudentList({ target: subjectSelect }); // Trigger initial load - - const editSubjectSelect = document.getElementById('editSubjectSelect'); - const allSubjects = await fetchJson('/subjects'); - populateSubjectSelect(editSubjectSelect, allSubjects); - - const editClassSelect = document.getElementById('editClassSelect'); - const allClasses = await fetchJson('/classes'); - populateClassSelect(editClassSelect, allClasses); - const roomSelect = document.getElementById("roomSelect"); - const rooms = await fetchJson("/rooms"); - populateRoomSelect(roomSelect, rooms); - roomSelect.addEventListener('change', populateRoomStudentList); - populateRoomStudentList({target: roomSelect}); + buildTeacherDashboard(await fetchTeacherClasses(teacherId), await fetchSubjects(teacherId)); + populateSubjectSelect('editSubjectSelect', await fetchAllSubjects()); + const editClassSelect = document.getElementById('editClassSelect'); + const allClasses = await fetchClasses(); + populateClassSelect(editClassSelect, allClasses); }); diff --git a/src/main/resources/js/site/student-database.js b/src/main/resources/js/site/student-database.js new file mode 100644 index 0000000..645737f --- /dev/null +++ b/src/main/resources/js/site/student-database.js @@ -0,0 +1,850 @@ +async function fetchJson(url, options) { + const res = await fetch(url, options); + if (res.ok){ + return await res.json(); + } +} +async function getJson(url) { + return await fetchJson(url); +} +async function getJsonWithPost(url, data) { + return await fetchJson(url, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(data) + }); +} +async function post(url, data) { + return await fetch(url, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(data) + }); +} +function openUrlWithPostParams(url, params) { + const form = document.createElement("form"); + form.setAttribute("method", "post"); + form.setAttribute("action", url); + + Object.keys(params).forEach((key) => { + const input = document.createElement("input"); + input.setAttribute("type", "hidden") + input.setAttribute("name", key) + input.setAttribute("value", params[key]) + form.appendChild(input) + }) + + const submitButton = document.createElement("button") + submitButton.setAttribute("type", "submit") + + form.appendChild(submitButton) + document.getElementsByTagName("body")[0].appendChild(form) + + submitButton.click() +} +async function fetchClasses() { + const classes = await fetchJson('/classes'); + return classes; +} +async function fetchMyClasses() { + const classes = await fetchJson('/myclasses'); + return classes; +} +async function fetchRooms() { + const rooms = await fetchJson('/rooms'); + return rooms; +} +async function fetchTeacherClasses(teacherId) { + const classes = await getJsonWithPost('/teacher-classes', { teacherId }); + return classes; +} +async function fetchSubjects(teacherId) { + const subjects = await getJsonWithPost('/teacher-subjects', { teacherId }); + return subjects; +} +async function fetchMySubjects() { + const subjects = await fetchJson('/mysubjects'); + return subjects; +} +async function fetchStudentSubjects(studentId) { + const subjects = await getJsonWithPost('/student-subjects', { studentId }); + return subjects; +} +async function fetchAllSubjects() { + const subjects = await fetchJson('/subjects'); + return subjects; +} +async function fetchStudentData(studentId) { + return await getJsonWithPost('/student-data', { studentId }); +} +async function fetchMyData() { + return await fetchJson('/mydata'); +} +async function fetchCurrentTopic(subjectId, studentId) { + return await getJsonWithPost('/current-topic', { subjectId, studentId }); +} +async function fetchMyCurrentTopic(subjectId) { + return await getJsonWithPost('/current-topic', { subjectId }); +} +async function fetchTopicList(subjectId, grade) { + return await getJsonWithPost('/topic-list', { subjectId, grade }); +} +async function fetchTasks(taskIds, studentId) { + return await getJsonWithPost('/tasks', { ids: taskIds, studentId }); +} +async function fetchModuleSettings(moduleKey) { + return (await getJsonWithPost('/get-module', { key: moduleKey })).settings; +} + +async function getStudents(classId) { + return await getJsonWithPost('/student-list', { classId }); +} +async function getStudentsBySubject(classId, subjectId) { + return await getJsonWithPost('/student-list', { classId, subjectId }); +} +async function getStudentsByRoom(room) { + return await getJsonWithPost('/get-students-by-room', { room }); +} +async function searchPartner(subjectId, topicId, classId, studentId) { + return await getJsonWithPost('/search-partner', { subjectId, topicId, classId, studentId}); +} +async function addSubjectRequest(subjectId, subjectRequest, studentId) { + return await post('/subject-request', { subjectId, subjectRequest, studentId }); +} +async function removeSubjectRequest(subjectId, subjectRequest, studentId) { + return await post('/subject-request', { subjectId, subjectRequest, studentId, remove: true }); +} +function viewStudent(studentId) { + // Add studentId to session storage + sessionStorage.setItem('selectedStudentId', studentId); + // Redirect to student dashboard + window.location.href = `/student`; +} +function viewSubject(subject) { + sessionStorage.setItem('currentSubject', JSON.stringify(subject)); + window.location.href = '/subject'; +} +function viewTeacher(teacher) { + sessionStorage.setItem('currentTeacher', JSON.stringify(teacher)); + window.location.href = '/teacher'; +} +function viewClass(cls) { + sessionStorage.setItem('currentClass', JSON.stringify(cls)); + window.location.href = '/class'; +} +async function changeGraduationLevel(studentId, newLevel) { + return await post('/change-graduation-level', { studentId: studentId, graduationLevel: newLevel }); +} +async function changeCurrentTopic(studentId, subjectId, topicId) { + return await post('/change-current-topic', { studentId, subjectId, topicId }); +} +async function completeTask(studentId, taskId) { + return await post('/complete-task', { studentId, taskId }); +} +async function cancelTask(studentId, taskId) { + return await post('/cancel-task', { studentId, taskId }); +} +async function lockTask(studentId, taskId) { + return await post('/lock-task', { studentId, taskId }); +} +async function reopenTask(studentId, taskId) { + return await post('/reopen-task', { studentId, taskId }); +} +async function beginTask(studentId, taskId) { + return await post('/begin-task', { studentId, taskId }); +} +async function updateRoom(studentId, room) { + return await post('/update-room', { studentId, room }); +} + +async function deleteClass(classId) { + return await post('/delete-class', { id: classId }); +} +async function deleteSubject(subjectId) { + return await post('/delete-subject', { id: subjectId }); +} +// Populating functions +async function populateTable(url, tableId, rowBuilder) { + const data = await fetchJson(url); + const tableBody = document.getElementById(tableId).getElementsByTagName('tbody')[0]; + tableBody.innerHTML = ''; + data.forEach(item => { + const newRow = tableBody.insertRow(); + rowBuilder(newRow, item); + }); +} +async function populateStudentTable(classId, tableId, rowBuilder) { + const students = await getStudents(classId); + const tableBody = document.getElementById(tableId).getElementsByTagName('tbody')[0]; + tableBody.innerHTML = ''; + students.forEach(item => { + const newRow = tableBody.insertRow(); + rowBuilder(newRow, item); + }); +} +async function populateSubjectStudentList(subjectSelectId, classSelectId, studentTableId) { + const subjectSelect = document.getElementById(subjectSelectId); + const classSelect = document.getElementById(classSelectId); + const selectedClassId = classSelect.value; + + if (!selectedClassId) { + subjectSelect.innerHTML = ""; // clear previous options if no class is selected + return; + } + + const students = await getStudentsBySubject(Number(selectedClassId), Number(subjectSelect.value)); + + const studentTable = document.getElementById(studentTableId).getElementsByTagName('tbody')[0]; + studentTable.innerHTML = ""; // clear previous rows + students.forEach(student => { + const row = document.createElement('tr'); + row.innerHTML = ` + ${student.name} + ${student.help ? "Ja" : "Nein"} + ${student.experiment ? "Ja" : "Nein"} + ${student.partner ? "Ja" : "Nein"} + ${student.test ? "Ja" : "Nein"} + + `; + studentTable.appendChild(row); + }); +} +async function populateRoomStudentList(room) { + const students = await getStudentsByRoom(room); + + const studentTable = document.getElementById("roomStudentTableBody"); + studentTable.innerHTML = ""; // clear previous rows + students.forEach(student => { + const row = document.createElement('tr'); + row.innerHTML = ` + ${student.name} + ${student.actionRequired ? "Ja" : "Nein"} + + `; + studentTable.appendChild(row); + }); +} +async function populatePartnerSubjectStudentList(subjectId, studentData) { + const topicId = (await fetchMyCurrentTopic(subjectId)).id; + const classId = studentData.schoolClass.id; + const studentId = studentData.id; + + const students = await searchPartner(subjectId, topicId, classId, studentId); + + const studentTable = document.getElementById("studentTableBody"); + studentTable.innerHTML = ""; // clear previous rows + students.forEach(student => { + const row = document.createElement('tr'); + row.innerHTML = ` + ${student.name} + ${student.room} + `; + studentTable.appendChild(row); + }); +} +async function populateTopicTable(tableId, subjectId, grade) { + const table = document.getElementById(tableId).getElementsByTagName("tbody")[0]; + table.innerHTML = ''; + const topics = await getJsonWithPost('/topic-list', { subjectId, grade}); + topics.forEach(topic => { + const row = document.createElement('tr'); + row.innerHTML = ` + ${topic.name} + ${topic.ratio} + ${topic.number} + ${topic.tasks.length} + ` + table.appendChild(row) + }) +} +function populateClassSelect(classSelect, classes) { + classSelect.innerHTML = ""; // clear previous options if any + classes.forEach(cls => { + const option = document.createElement('option'); + option.value = cls.classId || cls.id; + option.textContent = cls.name || cls.label; + classSelect.appendChild(option); + }); +} +async function populateSubjectSelect(subjectSelectId, subjects) { + const subjectSelect = document.getElementById(subjectSelectId); + subjectSelect.innerHTML = ''; // Clear existing options + subjects.forEach(function(subject) { + const option = document.createElement('option'); + option.value = subject.id; + option.textContent = subject.name; + subjectSelect.appendChild(option); + }); +} +async function populateTopicSelect(topicSelect, subjectId, grade, currentTopic) { + const topics = await fetchTopicList(subjectId, grade); + topicSelect.innerHTML = ''; // Clear existing options + topics.forEach(t => { + const option = document.createElement('option'); + option.value = t.id; + option.textContent = `${t.name} (${t.number})`; + option.selected = (currentTopic && currentTopic.id == t.id); + topicSelect.appendChild(option); + }); +} +async function populateGradeSelect(gradeSelectId, subjectId) { + const gradeSelect = document.getElementById(gradeSelectId); + gradeSelect.innerHTML = ''; + const grades = await getJsonWithPost('/grade-list', { subjectId }); + grades.forEach(grade => { + const option = document.createElement('option'); + option.text = grade; + option.value = grade; + gradeSelect.appendChild(option) + }) +} +async function populateRoomSelect(roomSelectId) { + const roomSelect = document.getElementById(roomSelectId); + roomSelect.innerHTML = ""; // clear previous options if any + const rooms = await fetchRooms(); + rooms.forEach(room => { + const option = document.createElement('option'); + option.value = room.label; + option.textContent = room.label; + roomSelect.appendChild(option); + }); +} +async function populateRoomSelectWithLevel(roomSelectId, graduationLevel) { + const roomSelect = document.getElementById(roomSelectId); + roomSelect.innerHTML = ""; // clear previous options if any + const rooms = await fetchRooms(); + rooms.forEach(room => { + if (room.minimumLevel <= graduationLevel){ + const option = document.createElement('option'); + option.value = room.label; + option.textContent = room.label; + roomSelect.appendChild(option); + } + }); +} +async function populateSubjectList(subjectListId, classId) { + const subjectList = document.getElementById(subjectListId); + const subjects = await fetchJson("/class-subjects", { + method: 'POST', + body: JSON.stringify({ classId }), + headers: { + 'Content-Type': 'application/json' + } + }); + subjectList.innerHTML = ''; // Clear existing items + subjects.forEach(function(subject) { + const listItem = document.createElement('li'); + listItem.textContent = subject.name; + subjectList.appendChild(listItem); + }); +} +async function populateGradeList(listId, subjectId) { + const list = document.getElementById(listId); + list.innerHTML = ''; + const grades = await getJsonWithPost('/grade-list', { subjectId }); + grades.forEach(grade => { + const li = document.createElement("li"); + li.textContent = grade; + li.style.cursor = 'pointer'; + li.title = 'Klicken, um die Klasenstufe zu entfernen'; + li.addEventListener('click', (e) => { + if (confirm('Soll die Klassenstufe wirklich von diesem Fach entfernt werden?')) { + openUrlWithPostParams('/delete-grade-from-subject', { + "subject": subjectId, + "grade": grade + }) + } + }) + list.appendChild(li) + }); +} +async function postDataAndDownload(url, data, filename) { + const response = await fetch(url, { + method: 'POST', + headers: { 'Content-Type': 'text/plain' }, + body: data + }); + + if (response.ok) { + const blob = await response.blob(); + const disposition = response.headers.get('Content-Disposition'); + if (disposition && disposition.includes('filename=')) { + filename = disposition.split('filename=')[1].split(';')[0].trim(); + } + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = filename + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); + window.location.reload() + } else if (response.status === 401) { + alert("Nicht autorisiert! Bitte melden Sie sich an."); + window.location.href = "/login"; + } else { + alert("Fehler beim Hochladen der Datei!"); + } +} +function createList(items, textBuilder, labelText, onClick) { + const label = document.createElement('h4'); + label.textContent = labelText; + const list = document.createElement('ul'); + items.forEach(item => { + const li = document.createElement('li'); + li.textContent = textBuilder(item); + if (typeof onClick === 'function') { + li.style.cursor = 'pointer'; + li.addEventListener('click', () => onClick(item, li)); + } + list.appendChild(li); + }); + return { label, list }; +} +function createTaskList(tasks, titleText, onClick) { + return createList(tasks, task => `${task.number} ${decodeEntities(task.name)} (Niveau ${task.niveau}, Gesamtanteil: ${Math.round(task.ratio * 10000) / 100}%)`, titleText, onClick); +} +async function buildTeacherDashboard(classes, subjects) { + async function onClassChange(event) { + populateStudentTable(Number(event.target.value), "studentTable", (row, student) => { + row.innerHTML = ` + ${student.name} + ${student.room} + ${graduationLevels[student.graduationLevel]} + + `; + }); + } + + const classSelect = document.getElementById('classSelect'); + const subjectClassSelect = document.getElementById('classSelectSubject'); + populateClassSelect(classSelect, classes); + populateClassSelect(subjectClassSelect, classes); + classSelect.addEventListener('change', onClassChange); + subjectClassSelect.addEventListener('change', (_) => populateSubjectStudentList('subjectSelect', 'classSelectSubject', 'subjectStudentTable')); + onClassChange({ target: classSelect }); // Trigger initial load + + const subjectSelect = document.getElementById('subjectSelect'); + populateSubjectSelect('subjectSelect', subjects); + subjectSelect.addEventListener('change', (_) => populateSubjectStudentList('subjectSelect', 'classSelectSubject', 'subjectStudentTable')); + populateSubjectStudentList('subjectSelect', 'classSelectSubject', 'subjectStudentTable'); // Trigger initial load + + await populateRoomSelect('roomSelect'); + document.getElementById('roomSelect').addEventListener('change', (e) => populateRoomStudentList(e.target.value)); + populateRoomStudentList(document.getElementById('roomSelect').value); // Trigger initial load +} +function createRequestButton(subject, type, label) { + const btn = document.createElement('button'); + btn.textContent = label; + + // Helper to check if this request is active + function isActive() { + return ( + studentData.currentRequests && + studentData.currentRequests[subject.id] && + studentData.currentRequests[subject.id].includes(type) + ); + } + + // Set initial state + function updateButton() { + if (isActive()) { + btn.classList.add('active-request'); + } else { + btn.classList.remove('active-request'); + } + } + updateButton(); + + btn.addEventListener('click', async () => { + if (isActive()) { + await removeSubjectRequest(subject.id, type, studentData.id); + // Update local state + if (studentData.currentRequests[subject.id]) { + studentData.currentRequests[subject.id] = studentData.currentRequests[subject.id].filter(t => t !== type); + if (studentData.currentRequests[subject.id].length === 0) { + delete studentData.currentRequests[subject.id]; + } + } + } else { + await addSubjectRequest(subject.id, type, studentData.id); + // Update local state + if (!studentData.currentRequests[subject.id]) { + studentData.currentRequests[subject.id] = []; + } + studentData.currentRequests[subject.id].push(type); + } + updateButton(); + }); + + return btn; +} +function setStudentInfo(studentData) { + document.getElementById('student-name').textContent = `${studentData.firstName} ${studentData.lastName}`; + document.getElementById('student-class').textContent = studentData.schoolClass.label; + document.getElementById('student-email').textContent = studentData.email; + const graduationLevels = ["Neustarter", "Starter", "Durchstarter", "Lernprofi"]; + document.getElementById('student-graduation').textContent = graduationLevels[studentData.graduationLevel]; +} +function getGrade(progress) { + if (progress >= 0.85) return 1; + if (progress >= 0.70) return 2; + if (progress >= 0.55) return 3; + if (progress >= 0.40) return 4; + if (progress >= 0.20) return 5; + return 6; +} +function getGradeLabel(grade) { + return [ + "", + "1 (Sehr gut)", + "2 (Gut)", + "3 (Befriedigend)", + "4 (Ausreichend)", + "5 (Mangelhaft)", + "6 (Ungenügend)" + ][grade]; +} +function getTaskColorClass(level) { + if (level === 1) return "level1"; + if (level === 2) return "level2"; + if (level === 3) return "level3"; + return "special"; +} +function createGradeScale() { + const scale = document.createElement('div'); + scale.className = 'grade-scale'; + [6,5,4,3,2,1].forEach(grade => { + const label = document.createElement('span'); + label.className = 'grade-label'; + label.textContent = getGradeLabel(grade); + scale.appendChild(label); + }); + return scale; +} + +function createBarChart(subject, subjectName, studentData, settings) { + const chart = document.createElement('div'); + chart.className = 'bar-chart'; + + const title = document.createElement('h3'); + title.textContent = subjectName; + chart.appendChild(title); + + // Grade scale + chart.appendChild(createGradeScale()); + + // Get all tasks for this subject (from completed, selected, and topic.tasks) + const completed = studentData.completedTasks.filter(t => (t.topic && t.topic.subject && t.topic.subject.name === subject.name) || (!t.topic && t.subject && t.subject == subject.name)); + const selected = studentData.selectedTasks.filter(t => t.topic && t.topic.id && t.topic.subject && t.topic.subject.name === subject.name); + // For demo: If you have all tasks for the subject, fetch them here. Otherwise, use completed+selected as all tasks. + let allTasks = [...completed, ...selected]; + console.log(`Creating bar chart for subject: ${subjectName}`, subject, { + completed: completed.length, + selected: selected.length, + allTasks: allTasks.length + }); + // Remove duplicates by id + allTasks = allTasks.filter((task, idx, arr) => arr.findIndex(t => t.id === task.id && t.name === task.name) === idx); + + // Sort by completion: completed first (by completion order if available), then selected, then others + allTasks.sort((a, b) => { + const aCompleted = completed.some(t => t.id === a.id); + const bCompleted = completed.some(t => t.id === b.id); + if (aCompleted && !bCompleted) return -1; + if (!aCompleted && bCompleted) return 1; + // Optionally: sort by task.number or id + return (a.number || 0) - (b.number || 0); + }); + + // Bar container + const bar = document.createElement('div'); + bar.className = 'bar-container'; + + allTasks.forEach(task => { + const div = document.createElement('div'); + div.className = 'bar-task ' + getTaskColorClass(task.niveau); + if (completed.some(t => t.id === task.id && t.name === task.name)) { + div.classList.add('completed'); + } else if (selected.some(t => t.id === task.id && t.name === task.name)) { + div.classList.add('selected', 'hatched'); + } + div.style.width = (100 * task.ratio) + '%'; + // Optional: show task number or name + const label = document.createElement('span'); + label.className = 'bar-task-label'; + label.textContent = task.number || ''; + div.appendChild(label); + bar.appendChild(div); + }); + + chart.appendChild(bar); + + // Progress and grade info + const progress = studentData.currentProgress && studentData.currentProgress[subjectName] + ? studentData.currentProgress[subjectName].progress : 0; + const predicted = studentData.predictedProgress && studentData.predictedProgress[subjectName] + ? studentData.predictedProgress[subjectName].predictedProgress : 0; + + const grade = settings.show_current_grade.value ? getGrade(progress) : 0; + const predictedGrade = getGrade(predicted); + + if (!settings || settings.show_current_progress.value) { + const gradeInfo = document.createElement('div'); + gradeInfo.className = 'grade-current'; + gradeInfo.innerHTML = `Aktueller Fortschritt: ${getGradeLabel(grade)} (${Math.round(progress * 100)}%)`; + chart.appendChild(gradeInfo); + } + + const predInfo = document.createElement('div'); + predInfo.className = 'grade-prediction'; + predInfo.innerHTML = `Prognose für Jahresende: ${getGradeLabel(predictedGrade)} (${Math.round(predicted * 100)}%)`; + chart.appendChild(predInfo); + + return chart; +} +function createPanel(header, bodyContent, loadCallback) { + const panel = document.createElement('div'); + panel.className = 'panel'; + + const headerElem = document.createElement('h3'); + headerElem.textContent = header; + panel.appendChild(headerElem); + + const body = document.createElement('div'); + body.className = 'panel-body'; + body.appendChild(bodyContent); + + panel.appendChild(body); + + function refreshPanel() { + bodyContent.innerHTML = ''; + headerElem.click(); // Re-trigger the header click to close the panel + panel.classList.remove('loaded'); // Reset loaded state + headerElem.click(); // Re-trigger the header click to load tasks + } + headerElem.addEventListener('click', async () => { + panel.classList.toggle('active'); + if (panel.classList.contains('loaded')) return; + panel.classList.add('loaded'); + loadCallback(headerElem, bodyContent) + }); + panel.refresh = refreshPanel; + return panel; +} +function createSubjectPanel(subject, studentData, teacherPerms) { + const body = document.createElement('div'); + function createRequestButtons(body) { + ['hilfe', 'partner', 'betreuung', 'gelingensnachweis'].forEach(type => { + const label = { + hilfe: 'Schüler braucht Hilfe', + partner: 'Schüler sucht einen Partner', + betreuung: 'Schüler braucht Betreuung für ein Experiment', + gelingensnachweis: 'Schüler ist bereit für den Gelingensnachweis' + }[type]; + + const btn = createRequestButton(subject, type, label); + body.appendChild(btn); + }); + } + const studentId = studentData.id; + + const panel = createPanel(subject.name, body, async (header, body) => { + body.innerHTML = ''; // Clear previous content + // Request buttons + createRequestButtons(body); + // Load current topic for this subject + const topic = await fetchCurrentTopic(subject.id, studentId); + + const topicTitle = document.createElement('p'); + if (teacherPerms) { + topicTitle.innerHTML = ``; + const topicSelect = document.createElement('select'); + topicSelect.id = "topicSelect"; + populateTopicSelect(topicSelect, subject.id, studentData.schoolClass.grade, topic); + topicSelect.addEventListener('change', async e => { + result = await changeCurrentTopic(studentId, subject.id, topicSelect.value); + if (result.ok) { + panel.refresh(); + } else { + alert('Fehler beim Ändern des Themas') + } + }); + topicSelect.class = 'topicSelect'; + topicTitle.appendChild(topicSelect); + } else { + topicTitle.textContent = "Aktuelles Thema: " + topic.name + } + body.appendChild(topicTitle); + + // Filter tasks for the current topic + const selectedTasks = studentData.selectedTasks.filter( + task => task.topic && task.topic.id === topic.id + ); + const completedTasks = studentData.completedTasks.filter( + task => task.topic && task.topic.id === topic.id + ); + const lockedTasks = studentData.lockedTasks.filter( + task => task.topic && task.topic.id === topic.id + ); + let allTasks = []; + if (Array.isArray(topic.tasks) && topic.tasks.length > 0) { + allTasks = await fetchTasks(topic.tasks, studentId); + } + + const otherTasks = allTasks.filter( + task => + !selectedTasks.some(t => t.id === task.id) && + !completedTasks.some(t => t.id === task.id) && + !lockedTasks.some(t => t.id === task.id) + ); + + // Current stage (selectedTasks) + const { label: selectedLabel, list: selectedList } = createTaskList(selectedTasks, 'Aktuelle Etappe:', async (task) => { + if (teacherPerms){ + const action = window.prompt( + 'Was möchten Sie tun?\n1: Als abgeschlossen markieren\n2: Aufgabe abbrechen\n3: Aufgabe sperren', + '1' + ); + if (action === '1') { + // Move to completed + completeTask(studentId, task.id); + // Update local state + studentData.selectedTasks = studentData.selectedTasks.filter(t => t.id !== task.id); + studentData.completedTasks.push(task); + } else if (action === '2') { + // Cancel task + cancelTask(studentId, task.id); + studentData.selectedTasks = studentData.selectedTasks.filter(t => t.id !== task.id); + // No need to push to completedTasks or otherTasks, UI will refresh + } else if (action === '3') { + lockTask(studentId, task.id); + studentData.selectedTasks = studentData.selectedTasks.filter(t => t.id !== task.id); + studentData.lockedTasks.push(task) + } + panel.refresh(); // Refresh the panel to show updated tasks + } else { + // Cancel task + cancelTask(studentId, task.id); + studentData.selectedTasks = studentData.selectedTasks.filter(t => t.id !== task.id); + // No need to push to completedTasks or otherTasks, UI will refresh + panel.refresh(); // Refresh the panel to show updated tasks + } + }); + body.appendChild(selectedLabel); + body.appendChild(selectedList); + + + // Completed stages + const { label: completedLabel, list: completedList } = createTaskList(completedTasks, 'Abgeschlossene Etappen:', async (task) => { + if (teacherPerms && window.confirm('Soll diese Aufgabe wirklich wieder in die offenen Aufgaben verschoben werden?')) { + reopenTask(studentId, task.id); + studentData.completedTasks = studentData.completedTasks.filter(t => t.id !== task.id); + // No need to push to otherTasks, UI will refresh + panel.refresh(); // Refresh the panel to show updated tasks + } + }); + body.appendChild(completedLabel); + body.appendChild(completedList); + + // locked stages + const { label: lockedLabel, list: lockedList } = createTaskList(lockedTasks, 'Gesperrte Etappen:', async (task) => { + if (teacherPerms && window.confirm('Soll diese Aufgabe wirklich wieder in die offenen Aufgaben verschoben werden?')) { + reopenTask(studentId, task.id); + studentData.lockedTasks = studentData.lockedTasks.filter(t => t.id !== task.id); + // No need to push to otherTasks, UI will refresh + panel.refresh(); // Refresh the panel to show updated tasks + } + }); + body.appendChild(lockedLabel); + body.appendChild(lockedList); + + // Other stages + const { label: otherLabel, list: otherList } = createTaskList(otherTasks, 'Weitere Etappen:', async (task) => { + beginTask(studentId, task.id); + studentData.selectedTasks.push(task); + panel.refresh(); // Refresh the panel to show updated tasks + }); + body.appendChild(otherLabel); + body.appendChild(otherList); + }); + return panel; +} +function decodeEntities(str) { + const txt = document.createElement("textarea"); + txt.innerHTML = str; + return txt.value; +} +function loadStudentDashboard(studentData, subjects, teacherPerms) { // Show student info + setStudentInfo(studentData); + + // Show rooms + populateRoomSelectWithLevel('room', studentData.graduationLevel); + + // Wait for the browser to render (next tick) + setTimeout(() => { + // Set room select value to current room + if (studentData.currentRoom && studentData.currentRoom.label) { + roomSelect.value = studentData.currentRoom.label; + } + }, 0); + + const roomSelect = document.getElementById('room'); + roomSelect.addEventListener('change', async () => updateRoom(studentData.id, roomSelect.value)); + + // Show subjects + const subjectList = document.getElementById('subject-list'); + subjects.forEach(subject => { + const panel = createSubjectPanel(subject, studentData, teacherPerms); + subjectList.appendChild(panel); + }); +} +async function loadStudentResultView(studentData) { + const settings = await fetchModuleSettings('result_view'); + + document.getElementById('student-name').textContent = `${studentData.firstName} ${studentData.lastName}`; + + // Get all subjects from progress keys + const subjectNames = Object.keys(studentData.currentProgress || {}); + // If you have subject objects, map them here; else, use names as fallback + // For demo: create fake subject objects + const subjects = subjectNames.map(name => ({ id: name, name })); + + const charts = document.getElementById('charts'); + subjects.forEach(subject => { + charts.appendChild(createBarChart(subject, subject.name, studentData, settings)); + }); +} +const graduationLevels = ["Neustarter", "Starter", "Durchstarter", "Lernprofi"]; +let currentClass = JSON.parse(sessionStorage.getItem('currentClass')); +document.addEventListener('DOMContentLoaded', async () => { + if (currentClass) { + for (const element of document.getElementsByClassName('classId')) { + element.textContent = currentClass.id; + element.value = currentClass.id; + } + for (const element of document.getElementsByClassName('className')) { + element.textContent = currentClass.label; + element.value = currentClass.label; + } + for (const element of document.getElementsByClassName('classGrade')) { + element.textContent = currentClass.grade; + element.value = currentClass.grade; + } + const studentList = document.getElementById('studentTableBody'); + if (studentList) { + populateStudentTable(currentClass.id, 'studentTable', (row, student) => { + row.innerHTML = ` + ${student.name} + ${student.room} + ${graduationLevels[student.graduationLevel]} + + `; + }) + } + if (document.getElementById('subjectSelect')) { + populateSubjectSelect('subjectSelect', await fetchAllSubjects()); + } + } +}) \ No newline at end of file diff --git a/src/main/resources/js/teacher/build_dashboard.js b/src/main/resources/js/teacher/build_dashboard.js index 2a3653d..6a84555 100644 --- a/src/main/resources/js/teacher/build_dashboard.js +++ b/src/main/resources/js/teacher/build_dashboard.js @@ -1,145 +1,3 @@ -async function fetchJson(url, options) { - const res = await fetch(url, options); - return await res.json(); -} - -async function fetchClasses() { - const classes = await fetchJson('/myclasses'); - return classes; -} - -function populateClassSelect(classSelect, classes) { - classSelect.innerHTML = ""; // clear previous options if any - classes.forEach(cls => { - const option = document.createElement('option'); - option.value = cls.classId; - option.textContent = cls.name; - classSelect.appendChild(option); - }); -} -function populateSubjectSelect(subjectSelect, subjects) { - subjectSelect.innerHTML = ""; // clear previous options if any - subjects.forEach(subject => { - const option = document.createElement('option'); - option.value = subject.subjectId || subject.id; // Use subjectId or id based on your API - option.textContent = subject.name; - subjectSelect.appendChild(option); - }); -} -function populateRoomSelect(roomSelect, rooms) { - roomSelect.innerHTML = ""; // clear previous options if any - rooms.forEach(room => { - const option = document.createElement('option'); - option.value = room.label; - option.textContent = room.label; - roomSelect.appendChild(option); - }); -} - -async function onClassChange(event) { - const selectedClassId = event.target.value; - const students = await fetchJson("/student-list", { - method: 'POST', - body: JSON.stringify({ classId: Number(selectedClassId) }), - headers: { - 'Content-Type': 'application/json' - } - }); - const studentTable = document.getElementById("studentTableBody"); - studentTable.innerHTML = ""; // clear previous rows - students.forEach(student => { - const row = document.createElement('tr'); - const graduationLevels = ["Neustarter", "Starter", "Durchstarter", "Lernprofi"]; - row.innerHTML = ` - ${student.name} - ${student.room} - ${graduationLevels[student.graduationLevel]} - - `; - studentTable.appendChild(row); - }); -} -async function populateSubjectStudentList(event) { - const subjectSelect = document.getElementById('subjectSelect'); - const classSelect = document.getElementById('classSelect'); - const selectedClassId = classSelect.value; - - if (!selectedClassId) { - subjectSelect.innerHTML = ""; // clear previous options if no class is selected - return; - } - - const students = await fetchJson("/student-list", { - method: 'POST', - body: JSON.stringify({ classId: Number(selectedClassId), subjectId: Number(subjectSelect.value) }), - headers: { - 'Content-Type': 'application/json' - } - }); - - const studentTable = document.getElementById("subjectStudentTableBody"); - studentTable.innerHTML = ""; // clear previous rows - students.forEach(student => { - const row = document.createElement('tr'); - row.innerHTML = ` - ${student.name} - ${student.help ? "Ja" : "Nein"} - ${student.experiment ? "Ja" : "Nein"} - ${student.partner ? "Ja" : "Nein"} - ${student.test ? "Ja" : "Nein"} - - `; - studentTable.appendChild(row); - }); -} -async function populateRoomStudentList(event) { - const room = event.target.value; - - const students = await fetchJson("/get-students-by-room", { - method: 'POST', - body: JSON.stringify({ room }), - headers: { - 'Content-Type': 'application/json' - } - }); - - const studentTable = document.getElementById("roomStudentTableBody"); - studentTable.innerHTML = ""; // clear previous rows - students.forEach(student => { - const row = document.createElement('tr'); - row.innerHTML = ` - ${student.name} - ${student.actionRequired ? "Ja" : "Nein"} - - `; - studentTable.appendChild(row); - }); -} - -function viewStudent(studentId) { - // Add studentId to session storage - sessionStorage.setItem('selectedStudentId', studentId); - // Redirect to student dashboard - window.location.href = `/student`; -} - document.addEventListener('DOMContentLoaded', async () => { - const classSelect = document.getElementById('classSelect'); - const subjectClassSelect = document.getElementById('classSelectSubject'); - const classes = await fetchClasses(); - populateClassSelect(classSelect, classes); - populateClassSelect(subjectClassSelect, classes); - classSelect.addEventListener('change', onClassChange); - subjectClassSelect.addEventListener('change', populateSubjectStudentList); - onClassChange({ target: classSelect }); // Trigger initial load - const subjectSelect = document.getElementById('subjectSelect'); - const subjects = await fetchJson('/mysubjects'); - populateSubjectSelect(subjectSelect, subjects); - subjectSelect.addEventListener('change', populateSubjectStudentList); - populateSubjectStudentList({ target: subjectSelect }); // Trigger initial load - const roomSelect = document.getElementById("roomSelect"); - const rooms = await fetchJson("/rooms"); - populateRoomSelect(roomSelect, rooms); - roomSelect.addEventListener('change', populateRoomStudentList); - populateRoomStudentList({target: roomSelect}); -}); + buildTeacherDashboard(fetchTeacherClasses(teacherId), fetchSubjects(teacherId)); +}); \ No newline at end of file diff --git a/src/main/resources/js/teacher/build_results.js b/src/main/resources/js/teacher/build_results.js index 8f611ae..38a6ec6 100644 --- a/src/main/resources/js/teacher/build_results.js +++ b/src/main/resources/js/teacher/build_results.js @@ -1,142 +1,17 @@ let studentId = Number(sessionStorage.getItem('selectedStudentId')); - -function getGrade(progress) { - if (progress >= 0.85) return 1; - if (progress >= 0.70) return 2; - if (progress >= 0.55) return 3; - if (progress >= 0.40) return 4; - if (progress >= 0.20) return 5; - return 6; -} - -function getGradeLabel(grade) { - return [ - "1 (Sehr gut)", - "2 (Gut)", - "3 (Befriedigend)", - "4 (Ausreichend)", - "5 (Mangelhaft)", - "6 (Ungenügend)" - ][grade - 1]; -} - -function getTaskColorClass(level) { - if (level === 1) return "level1"; - if (level === 2) return "level2"; - if (level === 3) return "level3"; - return "special"; -} - -function createGradeScale() { - const scale = document.createElement('div'); - scale.className = 'grade-scale'; - [6,5,4,3,2,1].forEach(grade => { - const label = document.createElement('span'); - label.className = 'grade-label'; - label.textContent = getGradeLabel(grade); - scale.appendChild(label); - }); - return scale; -} - -function createBarChart(subject, subjectName, studentData) { - const chart = document.createElement('div'); - chart.className = 'bar-chart'; - - const title = document.createElement('h3'); - title.textContent = subjectName; - chart.appendChild(title); - - // Grade scale - chart.appendChild(createGradeScale()); - - // Get all tasks for this subject (from completed, selected, and topic.tasks) - const completed = studentData.completedTasks.filter(t => (t.topic && t.topic.subject && t.topic.subject.name === subject.name) || (!t.topic && t.subject && t.subject == subject.name)); - const selected = studentData.selectedTasks.filter(t => t.topic && t.topic.id && t.topic.subject && t.topic.subject.name === subject.name); - // For demo: If you have all tasks for the subject, fetch them here. Otherwise, use completed+selected as all tasks. - let allTasks = [...completed, ...selected]; - console.log(`Creating bar chart for subject: ${subjectName}`, subject, { - completed: completed.length, - selected: selected.length, - allTasks: allTasks.length - }); - // Remove duplicates by id - allTasks = allTasks.filter((task, idx, arr) => arr.findIndex(t => t.id === task.id && t.name === task.name) === idx); - - // Sort by completion: completed first (by completion order if available), then selected, then others - allTasks.sort((a, b) => { - const aCompleted = completed.some(t => t.id === a.id); - const bCompleted = completed.some(t => t.id === b.id); - if (aCompleted && !bCompleted) return -1; - if (!aCompleted && bCompleted) return 1; - // Optionally: sort by task.number or id - return (a.number || 0) - (b.number || 0); - }); - - // Bar container - const bar = document.createElement('div'); - bar.className = 'bar-container'; - - allTasks.forEach(task => { - const div = document.createElement('div'); - div.className = 'bar-task ' + getTaskColorClass(task.niveau); - if (completed.some(t => t.id === task.id && t.name === task.name)) { - div.classList.add('completed'); - } else if (selected.some(t => t.id === task.id && t.name === task.name)) { - div.classList.add('selected', 'hatched'); - } - div.style.width = (100 * task.ratio) + '%'; - // Optional: show task number or name - const label = document.createElement('span'); - label.className = 'bar-task-label'; - label.textContent = task.number || ''; - div.appendChild(label); - bar.appendChild(div); - }); - - chart.appendChild(bar); - - // Progress and grade info - const progress = studentData.currentProgress && studentData.currentProgress[subjectName] - ? studentData.currentProgress[subjectName].progress : 0; - const predicted = studentData.predictedProgress && studentData.predictedProgress[subjectName] - ? studentData.predictedProgress[subjectName].predictedProgress : 0; - - const grade = getGrade(progress); - const predictedGrade = getGrade(predicted); - - const gradeInfo = document.createElement('div'); - gradeInfo.className = 'grade-current'; - gradeInfo.innerHTML = `Aktuelle Note: ${getGradeLabel(grade)} (${Math.round(progress * 100)}%)`; - chart.appendChild(gradeInfo); - - const predInfo = document.createElement('div'); - predInfo.className = 'grade-prediction'; - predInfo.innerHTML = `Prognose für Jahresende: ${getGradeLabel(predictedGrade)} (${Math.round(predicted * 100)}%)`; - chart.appendChild(predInfo); - - return chart; -} +let settings; document.addEventListener('DOMContentLoaded', async () => { - // Load student data (reuse endpoint from dashboard) - const res = await fetch('/student-data', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ studentId: studentId }) - }); - const studentData = await res.json(); - - document.getElementById('student-name').textContent = `${studentData.firstName} ${studentData.lastName}`; - - // Get all subjects from progress keys - const subjectNames = Object.keys(studentData.currentProgress || {}); - // If you have subject objects, map them here; else, use names as fallback - // For demo: create fake subject objects - const subjects = subjectNames.map(name => ({ id: name, name })); - - const charts = document.getElementById('charts'); - subjects.forEach(subject => { - charts.appendChild(createBarChart(subject, subject.name, studentData)); - }); + // Load student data + const studentData = await fetchStudentData(studentId); + + loadStudentResultView(studentData); + document.getElementById('studentId').value = studentId; + document.getElementById('download-student-results-csv').addEventListener('submit', async function(e) { + e.preventDefault(); + const form = e.target; + const studentId = Number(form['studentId'].value); + + postDataAndDownload('/student-results-csv', JSON.stringify({ studentId }), 'schueler_ergebnisse_' + studentId + '.csv'); + }); }); \ No newline at end of file diff --git a/src/main/resources/js/teacher/build_student.js b/src/main/resources/js/teacher/build_student.js index 17373f5..6b01ac2 100644 --- a/src/main/resources/js/teacher/build_student.js +++ b/src/main/resources/js/teacher/build_student.js @@ -2,357 +2,10 @@ let studentData = null; let studentId = Number(sessionStorage.getItem('selectedStudentId')); -function decodeEntities(str) { - const txt = document.createElement("textarea"); - txt.innerHTML = str; - return txt.value; -} - -function createTaskList(tasks, labelText, onClick) { - const label = document.createElement('h4'); - label.textContent = labelText; - const list = document.createElement('ul'); - tasks.forEach(task => { - const li = document.createElement('li'); - li.textContent = `${task.number} ${decodeEntities(task.name)} (Niveau ${task.niveau}, Gesamtanteil: ${Math.round(task.ratio * 10000) / 100}%)`; - if (typeof onClick === 'function') { - li.style.cursor = 'pointer'; - li.addEventListener('click', () => onClick(task, li)); - } - list.appendChild(li); - }); - return { label, list }; -} - -function createRequestButton(subject, type, label) { - const btn = document.createElement('button'); - btn.textContent = label; - - // Helper to check if this request is active - function isActive() { - return ( - studentData.currentRequests && - studentData.currentRequests[subject.id] && - studentData.currentRequests[subject.id].includes(type) - ); - } - - // Set initial state - function updateButton() { - if (isActive()) { - btn.classList.add('active-request'); - } else { - btn.classList.remove('active-request'); - } - } - updateButton(); - - btn.addEventListener('click', async () => { - if (isActive()) { - // Remove request - await fetch('/subject-request', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ subjectId: subject.id, subjectRequest: type, remove: true, studentId }) - }); - // Update local state - if (studentData.currentRequests[subject.id]) { - studentData.currentRequests[subject.id] = studentData.currentRequests[subject.id].filter(t => t !== type); - if (studentData.currentRequests[subject.id].length === 0) { - delete studentData.currentRequests[subject.id]; - } - } - } else { - // Add request - await fetch('/subject-request', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ subjectId: subject.id, subjectRequest: type, studentId }) - }); - // Update local state - if (!studentData.currentRequests[subject.id]) { - studentData.currentRequests[subject.id] = []; - } - studentData.currentRequests[subject.id].push(type); - } - updateButton(); - }); - - return btn; -} - -async function fetchJson(url, options) { - const res = await fetch(url, options); - if (res.ok){ - return await res.json(); - } -} - -function setStudentInfo(studentData) { - document.getElementById('student-name').textContent = `${studentData.firstName} ${studentData.lastName}`; - document.getElementById('student-class').textContent = studentData.schoolClass.label; - document.getElementById('student-email').textContent = studentData.email; - const graduationLevels = ["Neustarter", "Starter", "Durchstarter", "Lernprofi"]; - document.getElementById('student-graduation').textContent = graduationLevels[studentData.graduationLevel]; -} - -function populateRoomSelect(roomSelect, rooms, graduationLevel) { - roomSelect.innerHTML = ""; // clear previous options if any - rooms.forEach(room => { - if (graduationLevel >= room.minimumLevel) { - const option = document.createElement('option'); - option.value = room.label; - option.textContent = room.label; - roomSelect.appendChild(option); - } - }); -} - -function createPanel(subject, studentData) { - const panel = document.createElement('div'); - panel.className = 'subject-panel'; - - const header = document.createElement('h3'); - header.textContent = subject.name; - - const body = document.createElement('div'); - body.className = 'panel-body'; - - function createRequestButtons() { - ['hilfe', 'partner', 'betreuung', 'gelingensnachweis'].forEach(type => { - const label = { - hilfe: 'Schüler braucht Hilfe', - partner: 'Schüler sucht einen Partner', - betreuung: 'Schüler braucht Betreuung für ein Experiment', - gelingensnachweis: 'Schüler ist bereit für den Gelingensnachweis' - }[type]; - - const btn = createRequestButton(subject, type, label); - body.appendChild(btn); - }); - } - function refreshPanel() { - body.innerHTML = ''; - header.click(); // Re-trigger the header click to close the panel - panel.classList.remove('loaded'); // Reset loaded state - createRequestButtons(); - header.click(); // Re-trigger the header click to load tasks - } - - header.addEventListener('click', async () => { - panel.classList.toggle('active'); - if (panel.classList.contains('loaded')) return; - panel.classList.add('loaded'); - - // Load current topic for this subject - const topic = await fetchJson('/current-topic', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ subjectId: subject.id, studentId: studentId }) - }); - - // Load all topics for this subject - const topics = await fetchJson('/topic-list', { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ - subjectId: subject.id, grade: studentData.schoolClass.grade - }) - }) - - const topicTitle = document.createElement('p'); - topicTitle.innerHTML = ``; - const topicSelect = document.createElement('select'); - topics.forEach(t => { - const option = document.createElement('option'); - option.value = t.id; - option.textContent = `${t.name} (${t.number})`; - option.selected = (topic && topic.id == t.id); - topicSelect.appendChild(option); - }); - topicSelect.addEventListener('change', async e => { - result = await fetch('/change-current-topic', { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ subjectId: subject.id, studentId, topicId: Number(e.target.value) }) - }); - if (result.ok) { - refreshPanel(); - } else { - alert('Fehler beim Ändern des Themas') - } - }); - topicSelect.id = 'topicSelect'; - topicTitle.appendChild(topicSelect); - body.appendChild(topicTitle); - - // Filter tasks for the current topic - const selectedTasks = studentData.selectedTasks.filter( - task => task.topic && task.topic.id === topic.id - ); - const completedTasks = studentData.completedTasks.filter( - task => task.topic && task.topic.id === topic.id - ); - const lockedTasks = studentData.lockedTasks.filter( - task => task.topic && task.topic.id === topic.id - ); - let allTasks = []; - if (Array.isArray(topic.tasks) && topic.tasks.length > 0) { - allTasks = await fetchJson('/tasks', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ ids: topic.tasks, studentId: studentId }) - }); - } - - const otherTasks = allTasks.filter( - task => - !selectedTasks.some(t => t.id === task.id) && - !completedTasks.some(t => t.id === task.id) && - !lockedTasks.some(t => t.id === task.id) - ); - - // Current stage (selectedTasks) - const { label: selectedLabel, list: selectedList } = createTaskList(selectedTasks, 'Aktuelle Etappe:', async (task) => { - const action = window.prompt( - 'Was möchten Sie tun?\n1: Als abgeschlossen markieren\n2: Aufgabe abbrechen\n3: Aufgabe sperren', - '1' - ); - if (action === '1') { - // Move to completed - await fetch('/complete-task', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ taskId: task.id, studentId }) - }); - // Update local state - studentData.selectedTasks = studentData.selectedTasks.filter(t => t.id !== task.id); - studentData.completedTasks.push(task); - } else if (action === '2') { - // Cancel task - await fetch('/cancel-task', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ taskId: task.id, studentId }) - }); - studentData.selectedTasks = studentData.selectedTasks.filter(t => t.id !== task.id); - // No need to push to completedTasks or otherTasks, UI will refresh - } else if (action === '3') { - await fetch('/lock-task', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ taskId: task.id, studentId }) - }); - studentData.selectedTasks = studentData.selectedTasks.filter(t => t.id !== task.id); - studentData.lockedTasks.push(task) - } - refreshPanel(); // Refresh the panel to show updated tasks - }); - body.appendChild(selectedLabel); - body.appendChild(selectedList); - - // Completed stages - const { label: completedLabel, list: completedList } = createTaskList(completedTasks, 'Abgeschlossene Etappen:', async (task) => { - if (window.confirm('Soll diese Aufgabe wirklich wieder in die offenen Aufgaben verschoben werden?')) { - await fetch('/reopen-task', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ taskId: task.id, studentId }) - }); - studentData.completedTasks = studentData.completedTasks.filter(t => t.id !== task.id); - // No need to push to otherTasks, UI will refresh - refreshPanel(); // Refresh the panel to show updated tasks - } - }); - body.appendChild(completedLabel); - body.appendChild(completedList); - - // locked stages - const { label: lockedLabel, list: lockedList } = createTaskList(lockedTasks, 'Gesperrte Etappen:', async (task) => { - if (window.confirm('Soll diese Aufgabe wirklich wieder in die offenen Aufgaben verschoben werden?')) { - await fetch('/reopen-task', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ taskId: task.id, studentId }) - }); - studentData.lockedTasks = studentData.lockedTasks.filter(t => t.id !== task.id); - // No need to push to otherTasks, UI will refresh - refreshPanel(); // Refresh the panel to show updated tasks - } - }); - body.appendChild(lockedLabel); - body.appendChild(lockedList); - - // Other stages - const { label: otherLabel, list: otherList } = createTaskList(otherTasks, 'Weitere Etappen:', async (task) => { - await fetch('/begin-task', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ taskId: task.id, studentId }) - }); - studentData.selectedTasks.push(task); - refreshPanel(); // Refresh the panel to show updated tasks - }); - body.appendChild(otherLabel); - body.appendChild(otherList); - }); - - // Request buttons - createRequestButtons(); - - panel.appendChild(header); - panel.appendChild(body); - return panel; -} - document.addEventListener('DOMContentLoaded', async () => { - // Load base data - studentData = await fetchJson('/student-data', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ studentId: studentId }) - }); - const rooms = await fetchJson('/rooms', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ studentId: studentId }) - }); - const subjects = await fetchJson('/student-subjects', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ studentId: studentId }) - }); - - // Show student info - setStudentInfo(studentData); - - // Show rooms - const roomSelect = document.getElementById('room'); - populateRoomSelect(roomSelect, rooms, studentData.graduationLevel); - - // Wait for the browser to render (next tick) - setTimeout(() => { - if (studentData.currentRoom && studentData.currentRoom.label) { - roomSelect.value = studentData.currentRoom.label; - } - }, 0); - - roomSelect.addEventListener('change', async () => { - await fetch('/update-room', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ room: roomSelect.value, studentId }) - }); - }); + // Load base data + studentData = await fetchStudentData(studentId); + const subjects = await fetchStudentSubjects(studentId); - // Show subjects - const subjectList = document.getElementById('subject-list'); - subjects.forEach(subject => { - const panel = createPanel(subject, studentData); - subjectList.appendChild(panel); - }); + loadStudentDashboard(studentData, subjects, true); }); diff --git a/src/main/resources/js/user/build_dashboard.js b/src/main/resources/js/user/build_dashboard.js index f26ff17..adf4f03 100644 --- a/src/main/resources/js/user/build_dashboard.js +++ b/src/main/resources/js/user/build_dashboard.js @@ -1,264 +1,9 @@ let studentData = null; -function decodeEntities(str) { - const txt = document.createElement("textarea"); - txt.innerHTML = str; - return txt.value; -} - -function createTaskList(tasks, labelText, onClick) { - const label = document.createElement('h4'); - label.textContent = labelText; - const list = document.createElement('ul'); - tasks.forEach(task => { - const li = document.createElement('li'); - li.textContent = `${task.number} ${decodeEntities(task.name)} (Niveau ${task.niveau}, Gesamtanteil: ${Math.round(task.ratio * 10000) / 100}%)`; - if (typeof onClick === 'function') { - li.style.cursor = 'pointer'; - li.addEventListener('click', () => onClick(task, li)); - } - list.appendChild(li); - }); - return { label, list }; -} - -function createRequestButton(subject, type, label) { - const btn = document.createElement('button'); - btn.textContent = label; - - // Helper to check if this request is active - function isActive() { - return ( - studentData.currentRequests && - studentData.currentRequests[subject.id] && - studentData.currentRequests[subject.id].includes(type) - ); - } - - // Set initial state - function updateButton() { - if (isActive()) { - btn.classList.add('active-request'); - } else { - btn.classList.remove('active-request'); - } - } - updateButton(); - - btn.addEventListener('click', async () => { - if (isActive()) { - // Remove request - await fetch('/subject-request', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ subjectId: subject.id, subjectRequest: type, remove: true }) - }); - // Update local state - if (studentData.currentRequests[subject.id]) { - studentData.currentRequests[subject.id] = studentData.currentRequests[subject.id].filter(t => t !== type); - if (studentData.currentRequests[subject.id].length === 0) { - delete studentData.currentRequests[subject.id]; - } - } - } else { - // Add request - await fetch('/subject-request', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ subjectId: subject.id, subjectRequest: type }) - }); - // Update local state - if (!studentData.currentRequests[subject.id]) { - studentData.currentRequests[subject.id] = []; - } - studentData.currentRequests[subject.id].push(type); - } - updateButton(); - }); - - return btn; -} - -async function fetchJson(url, options) { - const res = await fetch(url, options); - return await res.json(); -} - -function setStudentInfo(studentData) { - document.getElementById('student-name').textContent = `${studentData.firstName} ${studentData.lastName}`; - document.getElementById('student-class').textContent = studentData.schoolClass.label; - document.getElementById('student-email').textContent = studentData.email; - const graduationLevels = ["Neustarter", "Starter", "Durchstarter", "Lernprofi"]; - document.getElementById('student-graduation').textContent = graduationLevels[studentData.graduationLevel]; -} - -function populateRoomSelect(roomSelect, rooms, graduationLevel) { - roomSelect.innerHTML = ""; // clear previous options if any - rooms.forEach(room => { - if (graduationLevel >= room.minimumLevel) { - const option = document.createElement('option'); - option.value = room.label; - option.textContent = room.label; - roomSelect.appendChild(option); - } - }); -} - -function createPanel(subject, studentData) { - const panel = document.createElement('div'); - panel.className = 'subject-panel'; - - const header = document.createElement('h3'); - header.textContent = subject.name; - - const body = document.createElement('div'); - body.className = 'panel-body'; - - function createRequestButtons() { - ['hilfe', 'partner', 'betreuung', 'gelingensnachweis'].forEach(type => { - const label = { - hilfe: 'Ich brauche Hilfe', - partner: 'Ich suche einen Partner', - betreuung: 'Ich brauche Betreuung für ein Experiment', - gelingensnachweis: 'Ich bin bereit für den Gelingensnachweis' - }[type]; - - const btn = createRequestButton(subject, type, label); - body.appendChild(btn); - }); - } - function refreshPanel() { - body.innerHTML = ''; - header.click(); // Re-trigger the header click to close the panel - panel.classList.remove('loaded'); // Reset loaded state - createRequestButtons(); - header.click(); // Re-trigger the header click to load tasks - } - - header.addEventListener('click', async () => { - panel.classList.toggle('active'); - if (panel.classList.contains('loaded')) return; - panel.classList.add('loaded'); - - // Load current topic for this subject - const topic = await fetchJson('/current-topic', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ subjectId: subject.id }) - }); - - const topicTitle = document.createElement('p'); - topicTitle.innerHTML = `Aktuelles Thema: ${topic.name} (${topic.number})`; - body.appendChild(topicTitle); - - // Filter tasks for the current topic - const selectedTasks = studentData.selectedTasks.filter( - task => task.topic && task.topic.id === topic.id - ); - const completedTasks = studentData.completedTasks.filter( - task => task.topic && task.topic.id === topic.id - ); - const lockedTasks = studentData.completedTasks.filter( - task => task.topic && task.topic.id === topic.id - ) - let allTasks = []; - if (Array.isArray(topic.tasks) && topic.tasks.length > 0) { - allTasks = await fetchJson('/tasks', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ ids: topic.tasks }) - }); - } - - const otherTasks = allTasks.filter( - task => - !selectedTasks.some(t => t.id === task.id) && - !completedTasks.some(t => t.id === task.id) && - !lockedTasks.some(t => t.id === task.id) - ); - - // Current stage (selectedTasks) - const { label: selectedLabel, list: selectedList } = createTaskList(selectedTasks, 'Aktuelle Etappe:', async (task) => { - if (window.confirm(`Möchtest du die Etappe "${task.name}" wirklich abbrechen?`)) { - console.log(`Abbrechen der Etappe: ${task.name} (ID: ${task.id})`); - // Cancel task - await fetch('/cancel-task', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ taskId: task.id }) - }); - studentData.selectedTasks = studentData.selectedTasks.filter(t => t.id !== task.id); - // No need to push to completedTasks or otherTasks, UI will refresh - refreshPanel(); // Refresh the panel to show updated tasks - } - }); - body.appendChild(selectedLabel); - body.appendChild(selectedList); - - // Completed stages - const { label: completedLabel, list: completedList } = createTaskList(completedTasks, 'Abgeschlossene Etappen:'); - body.appendChild(completedLabel); - body.appendChild(completedList); - - // Stages locked by the teacher - const { label: lockedLabel, list: lockedList} = createTaskList(lockedTasks, 'Gesperrte Etappen:'); - body.appendChild(lockedLabel); - body.appendChild(lockedList); - - // Other stages - const { label: otherLabel, list: otherList } = createTaskList(otherTasks, 'Weitere Etappen:', async (task) => { - await fetch('/begin-task', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ taskId: task.id }) - }); - studentData.selectedTasks.push(task); - refreshPanel(); // Refresh the panel to show updated tasks - }); - body.appendChild(otherLabel); - body.appendChild(otherList); - }); - - // Request buttons - createRequestButtons(); - - panel.appendChild(header); - panel.appendChild(body); - return panel; -} - document.addEventListener('DOMContentLoaded', async () => { - // Load base data - studentData = await fetchJson('/mydata'); - const rooms = await fetchJson('/rooms'); - const subjects = await fetchJson('/mysubjects'); - - // Show student info - setStudentInfo(studentData); - - // Show rooms - const roomSelect = document.getElementById('room'); - populateRoomSelect(roomSelect, rooms, studentData.graduationLevel); - - // Wait for the browser to render (next tick) - setTimeout(() => { - if (studentData.currentRoom && studentData.currentRoom.label) { - roomSelect.value = studentData.currentRoom.label; - } - }, 0); - - roomSelect.addEventListener('change', async () => { - await fetch('/update-room', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ room: roomSelect.value }) - }); - }); + // Load base data + studentData = await fetchMyData(); + const subjects = await fetchMySubjects(); - // Show subjects - const subjectList = document.getElementById('subject-list'); - subjects.forEach(subject => { - const panel = createPanel(subject, studentData); - subjectList.appendChild(panel); - }); + loadStudentDashboard(studentData, subjects); }); diff --git a/src/main/resources/js/user/build_partner_search.js b/src/main/resources/js/user/build_partner_search.js index d820f00..748eb26 100644 --- a/src/main/resources/js/user/build_partner_search.js +++ b/src/main/resources/js/user/build_partner_search.js @@ -1,53 +1,8 @@ -async function fetchJson(url, options) { - const res = await fetch(url, options); - return await res.json(); -} +const studentData = fetchMyData(); -const studentData = fetchJson("/mydata"); - -function populateSubjectSelect(subjectSelect, subjects) { - console.log("Populating subject select with subjects:", subjects); - subjectSelect.innerHTML = ""; // clear previous options if any - subjects.forEach(subject => { - const option = document.createElement('option'); - option.value = subject.subjectId || subject.id; // Use subjectId or id based on your API - option.textContent = subject.name; - subjectSelect.appendChild(option); - }); -} -async function populateSubjectStudentList(event) { - const subjectId = Number(event.target.value); - const topicId = (await fetchJson('/current-topic', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ subjectId }) - })).id; - const classId = (await studentData).schoolClass.id; - const studentId = (await studentData).id; - - const students = await fetchJson("/search-partner", { - method: 'POST', - body: JSON.stringify({ subjectId, topicId, classId, studentId }), - headers: { - 'Content-Type': 'application/json' - } - }); - - const studentTable = document.getElementById("studentTableBody"); - studentTable.innerHTML = ""; // clear previous rows - students.forEach(student => { - const row = document.createElement('tr'); - row.innerHTML = ` - ${student.name} - ${student.room} - `; - studentTable.appendChild(row); - }); -} document.addEventListener('DOMContentLoaded', async (_) => { - const subjectSelect = document.getElementById('subjectSelect'); const subjects = await fetchJson('/mysubjects'); - populateSubjectSelect(subjectSelect, subjects); - subjectSelect.addEventListener('change', populateSubjectStudentList); - populateSubjectStudentList({ target: subjectSelect }); + populateSubjectSelect('subjectSelect', subjects); + subjectSelect.addEventListener('change', async (e) => populatePartnerSubjectStudentList(e.target.value, await studentData)); + populatePartnerSubjectStudentList(subjectSelect.value, await studentData); }) \ No newline at end of file diff --git a/src/main/resources/js/user/build_results.js b/src/main/resources/js/user/build_results.js index c448355..cd6300c 100644 --- a/src/main/resources/js/user/build_results.js +++ b/src/main/resources/js/user/build_results.js @@ -1,136 +1,8 @@ -function getGrade(progress) { - if (progress >= 0.85) return 1; - if (progress >= 0.70) return 2; - if (progress >= 0.55) return 3; - if (progress >= 0.40) return 4; - if (progress >= 0.20) return 5; - return 6; -} - -function getGradeLabel(grade) { - return [ - "1 (Sehr gut)", - "2 (Gut)", - "3 (Befriedigend)", - "4 (Ausreichend)", - "5 (Mangelhaft)", - "6 (Ungenügend)" - ][grade - 1]; -} - -function getTaskColorClass(level) { - if (level === 1) return "level1"; - if (level === 2) return "level2"; - if (level === 3) return "level3"; - return "special"; -} - -function createGradeScale() { - const scale = document.createElement('div'); - scale.className = 'grade-scale'; - [6,5,4,3,2,1].forEach(grade => { - const label = document.createElement('span'); - label.className = 'grade-label'; - label.textContent = getGradeLabel(grade); - scale.appendChild(label); - }); - return scale; -} - -function createBarChart(subject, subjectName, studentData) { - const chart = document.createElement('div'); - chart.className = 'bar-chart'; - - const title = document.createElement('h3'); - title.textContent = subjectName; - chart.appendChild(title); - - // Grade scale - chart.appendChild(createGradeScale()); - - // Get all tasks for this subject (from completed, selected, and topic.tasks) - const completed = studentData.completedTasks.filter(t => (t.topic && t.topic.subject && t.topic.subject.name === subject.name) || (!t.topic && t.subject && t.subject == subject.name)); - const selected = studentData.selectedTasks.filter(t => t.topic && t.topic.id && t.topic.subject && t.topic.subject.name === subject.name); - // For demo: If you have all tasks for the subject, fetch them here. Otherwise, use completed+selected as all tasks. - let allTasks = [...completed, ...selected]; - console.log(`Creating bar chart for subject: ${subjectName}`, subject, { - completed: completed.length, - selected: selected.length, - allTasks: allTasks.length - }); - // Remove duplicates by id - allTasks = allTasks.filter((task, idx, arr) => arr.findIndex(t => t.id === task.id && t.name === task.name) === idx); - - // Sort by completion: completed first (by completion order if available), then selected, then others - allTasks.sort((a, b) => { - const aCompleted = completed.some(t => t.id === a.id); - const bCompleted = completed.some(t => t.id === b.id); - if (aCompleted && !bCompleted) return -1; - if (!aCompleted && bCompleted) return 1; - // Optionally: sort by task.number or id - return (a.number || 0) - (b.number || 0); - }); - - // Bar container - const bar = document.createElement('div'); - bar.className = 'bar-container'; - - allTasks.forEach(task => { - const div = document.createElement('div'); - div.className = 'bar-task ' + getTaskColorClass(task.niveau); - if (completed.some(t => t.id === task.id && t.name === task.name)) { - div.classList.add('completed'); - } else if (selected.some(t => t.id === task.id && t.name === task.name)) { - div.classList.add('selected', 'hatched'); - } - div.style.width = (100 * task.ratio) + '%'; - // Optional: show task number or name - const label = document.createElement('span'); - label.className = 'bar-task-label'; - label.textContent = task.number || ''; - div.appendChild(label); - bar.appendChild(div); - }); - - chart.appendChild(bar); - - // Progress and grade info - const progress = studentData.currentProgress && studentData.currentProgress[subjectName] - ? studentData.currentProgress[subjectName].progress : 0; - const predicted = studentData.predictedProgress && studentData.predictedProgress[subjectName] - ? studentData.predictedProgress[subjectName].predictedProgress : 0; - - const grade = getGrade(progress); - const predictedGrade = getGrade(predicted); - - const gradeInfo = document.createElement('div'); - gradeInfo.className = 'grade-current'; - gradeInfo.innerHTML = `Aktuelle Note: ${getGradeLabel(grade)} (${Math.round(progress * 100)}%)`; - chart.appendChild(gradeInfo); - - const predInfo = document.createElement('div'); - predInfo.className = 'grade-prediction'; - predInfo.innerHTML = `Prognose für Jahresende: ${getGradeLabel(predictedGrade)} (${Math.round(predicted * 100)}%)`; - chart.appendChild(predInfo); - - return chart; -} +let settings; document.addEventListener('DOMContentLoaded', async () => { - // Load student data (reuse endpoint from dashboard) - const res = await fetch('/mydata'); - const studentData = await res.json(); - - document.getElementById('student-name').textContent = `${studentData.firstName} ${studentData.lastName}`; - - // Get all subjects from progress keys - const subjectNames = Object.keys(studentData.currentProgress || {}); - // If you have subject objects, map them here; else, use names as fallback - // For demo: create fake subject objects - const subjects = subjectNames.map(name => ({ id: name, name })); - - const charts = document.getElementById('charts'); - subjects.forEach(subject => { - charts.appendChild(createBarChart(subject, subject.name, studentData)); - }); + // Load student data (reuse endpoint from dashboard) + const studentData = await fetchMyData(); + + loadStudentResultView(studentData); }); \ No newline at end of file diff --git a/src/main/resources/meta/paths/get_paths.json b/src/main/resources/meta/paths/get_paths.json new file mode 100644 index 0000000..c697c80 --- /dev/null +++ b/src/main/resources/meta/paths/get_paths.json @@ -0,0 +1,326 @@ +{ + "/mydata": { + "type": "GET", + "handler_type": "SQLRequestHandler", + "namespaces": ["sql"], + "context": "virtual", + "access_level": "user" + }, + "/rooms": { + "type": "GET", + "handler_type": "SQLRequestHandler", + "namespaces": ["sql"], + "context": "virtual", + "access_level": "public" + }, + "/mysubjects": { + "type": "GET", + "handler_type": "SQLRequestHandler", + "namespaces": ["sql"], + "context": "virtual", + "access_level": "user" + }, + "/myclasses": { + "type": "GET", + "handler_type": "SQLRequestHandler", + "namespaces": ["sql"], + "context": "virtual", + "access_level": "user" + }, + "/teachers": { + "type": "GET", + "handler_type": "SQLRequestHandler", + "namespaces": ["sql"], + "context": "virtual", + "access_level": "admin" + }, + "/students": { + "type": "GET", + "handler_type": "SQLRequestHandler", + "namespaces": ["sql"], + "context": "virtual", + "access_level": "admin" + }, + "/subjects": { + "type": "GET", + "handler_type": "SQLRequestHandler", + "namespaces": ["sql"], + "context": "virtual", + "access_level": "public" + }, + "/classes": { + "type": "GET", + "handler_type": "SQLRequestHandler", + "namespaces": ["sql"], + "context": "virtual", + "access_level": "admin" + }, + "/all-student-results": { + "type": "GET", + "handler_type": "SQLRequestHandler", + "namespaces": ["sql"], + "context": "virtual", + "access_level": "admin" + }, + + "/": { + "type": "GET", + "handler_type": "TemplatingFileRequestHandler", + "namespaces": ["site"], + "context": "html", + "access_level": "public" + }, + "/login": { + "type": "GET", + "handler_type": "TemplatingFileRequestHandler", + "namespaces": ["site"], + "context": "html", + "access_level": "public" + }, + + "/dashboard": { + "type": "GET", + "handler_type": "TemplatingFileRequestHandler", + "namespaces": ["admin", "teacher", "user"], + "context": "html", + "access_level": "user" + }, + "/results": { + "type": "GET", + "handler_type":"TemplatingFileRequestHandler", + "namespaces": ["user"], + "context": "html", + "access_level": "student" + }, + "/partner_search": { + "type": "GET", + "handler_type":"TemplatingFileRequestHandler", + "namespaces": ["user"], + "context": "html", + "access_level": "student" + }, + + "/student": { + "type": "GET", + "handler_type": "TemplatingFileRequestHandler", + "namespaces": ["teacher", "admin"], + "context": "html", + "access_level": "teacher" + }, + "/student-results": { + "type": "GET", + "handler_type": "TemplatingFileRequestHandler", + "namespaces": ["teacher", "admin"], + "context": "html", + "access_level": "teacher" + }, + + "/modules": { + "type": "GET", + "handler_type": "TemplatingFileRequestHandler", + "namespaces": ["admin"], + "context": "html", + "access_level": "admin" + }, + "/manage_students": { + "type": "GET", + "handler_type": "TemplatingFileRequestHandler", + "namespaces": ["admin"], + "context": "html", + "access_level": "admin" + }, + "/manage_teachers": { + "type": "GET", + "handler_type": "TemplatingFileRequestHandler", + "namespaces": ["admin"], + "context": "html", + "access_level": "admin" + }, + "/manage_classes": { + "type": "GET", + "handler_type": "TemplatingFileRequestHandler", + "namespaces": ["admin"], + "context": "html", + "access_level": "admin" + }, + "/manage_subjects": { + "type": "GET", + "handler_type": "TemplatingFileRequestHandler", + "namespaces": ["admin"], + "context": "html", + "access_level": "admin" + }, + "/manage_rooms": { + "type": "GET", + "handler_type": "TemplatingFileRequestHandler", + "namespaces": ["admin"], + "context": "html", + "access_level": "admin" + }, + "/teacher": { + "type": "GET", + "handler_type": "TemplatingFileRequestHandler", + "namespaces": ["admin"], + "context": "html", + "access_level": "admin" + }, + "/room": { + "type": "GET", + "handler_type": "TemplatingFileRequestHandler", + "namespaces": ["admin"], + "context": "html", + "access_level": "admin" + }, + "/subject": { + "type": "GET", + "handler_type": "TemplatingFileRequestHandler", + "namespaces": ["admin"], + "context": "html", + "access_level": "admin" + }, + "/class": { + "type": "GET", + "handler_type": "TemplatingFileRequestHandler", + "namespaces": ["admin"], + "context": "html", + "access_level": "admin" + }, + "/teacher-classes": { + "type": "GET", + "handler_type": "TemplatingFileRequestHandler", + "namespaces": ["admin"], + "context": "html", + "access_level": "admin" + }, + "/teacher-subjects": { + "type": "GET", + "handler_type": "TemplatingFileRequestHandler", + "namespaces": ["admin"], + "context": "html", + "access_level": "admin" + }, + "/teacher-students": { + "type": "GET", + "handler_type": "TemplatingFileRequestHandler", + "namespaces": ["admin"], + "context": "html", + "access_level": "admin" + }, + "/teacher-results": { + "type": "GET", + "handler_type": "TemplatingFileRequestHandler", + "namespaces": ["admin"], + "context": "html", + "access_level": "admin" + }, + "/teacher-dashboard": { + "type": "GET", + "handler_type": "TemplatingFileRequestHandler", + "namespaces": ["admin"], + "context": "html", + "access_level": "admin" + }, + + "/student-database.js": { + "type": "GET", + "handler_type": "FileRequestHandler", + "namespaces": ["site"], + "context": "js", + "access_level": "public" + }, + + "/build_dashboard.js": { + "type": "GET", + "handler_type": "FileRequestHandler", + "namespaces": ["user", "teacher", "admin"], + "context": "js", + "access_level": "user" + }, + "/build_results.js": { + "type": "GET", + "handler_type": "FileRequestHandler", + "namespaces": ["user", "teacher"], + "context": "js", + "access_level": "user" + }, + "/build_partner_search.js": { + "type": "GET", + "handler_type": "FileRequestHandler", + "namespaces": ["user", "teacher", "admin"], + "context": "js", + "access_level": "user" + }, + + "/build_student.js": { + "type": "GET", + "handler_type": "FileRequestHandler", + "namespaces": ["teacher"], + "context": "js", + "access_level": "teacher" + }, + + "/build_modules.js": { + "type": "GET", + "handler_type": "FileRequestHandler", + "namespaces": ["admin"], + "context": "js", + "access_level": "admin" + }, + "/build_teacher.js": { + "type": "GET", + "handler_type": "FileRequestHandler", + "namespaces": ["admin"], + "context": "js", + "access_level": "admin" + }, + "/build_room.js": { + "type": "GET", + "handler_type": "FileRequestHandler", + "namespaces": ["admin"], + "context": "js", + "access_level": "admin" + }, + "/build_subject.js": { + "type": "GET", + "handler_type": "FileRequestHandler", + "namespaces": ["admin"], + "context": "js", + "access_level": "admin" + }, + "/build_class.js": { + "type": "GET", + "handler_type": "FileRequestHandler", + "namespaces": ["admin"], + "context": "js", + "access_level": "admin" + }, + "/build_teacher_dashboard.js": { + "type": "GET", + "handler_type": "FileRequestHandler", + "namespaces": ["admin"], + "context": "js", + "access_level": "admin" + }, + + "/style.css": { + "type": "GET", + "handler_type": "FileRequestHandler", + "namespaces": ["site"], + "context": "css", + "access_level": "public" + }, + "/results.css": { + "type": "GET", + "handler_type": "FileRequestHandler", + "namespaces": ["site"], + "context": "css", + "access_level": "public" + }, + "/favicon.ico": { + "type": "GET", + "handler_type": "FileRequestHandler", + "namespaces": ["icons"], + "context": "imgs", + "access_level": "public" + } +} \ No newline at end of file diff --git a/src/main/resources/meta/paths/spaces.json b/src/main/resources/meta/paths/spaces.json new file mode 100644 index 0000000..e1f899a --- /dev/null +++ b/src/main/resources/meta/paths/spaces.json @@ -0,0 +1,22 @@ +{ + "public_spaces": [ + "error", + "site", + "icons" + ], + "user_space": "user", + "teacher_space": "teacher", + "admin_space": "admin", + "public_locations": [ + "rooms", + "subjects" + ], + "user_locations": [], + "teacher_locations": [], + "admin_locations": [ + "students", + "teachers", + "classes", + "all-student-results" + ] +} \ No newline at end of file diff --git a/src/main/resources/templates/html/class_info.html b/src/main/resources/templates/html/class_info.html new file mode 100644 index 0000000..ca36e3a --- /dev/null +++ b/src/main/resources/templates/html/class_info.html @@ -0,0 +1,55 @@ +
+

Klassenübersicht

+
+ + +
+
+

Schüler in der Klasse:

+ + + + + + + + + + +
NameRaumGraduierung
+
+ + + + +
+
+ + + + +
+ +
+
\ No newline at end of file diff --git a/src/main/resources/templates/html/file_import.html b/src/main/resources/templates/html/file_import.html new file mode 100644 index 0000000..1d4649a --- /dev/null +++ b/src/main/resources/templates/html/file_import.html @@ -0,0 +1,33 @@ +
+

%{header}

+

%{note}

+
+ + +
+ +
\ No newline at end of file diff --git a/src/main/resources/templates/html/room_info.html b/src/main/resources/templates/html/room_info.html new file mode 100644 index 0000000..46a5a9b --- /dev/null +++ b/src/main/resources/templates/html/room_info.html @@ -0,0 +1,20 @@ +
+

Raumübersicht

+
+ + +
+
+

Schüler im Raum:

+ + + + + + + + + +
NameBraucht Unterstützung
+
+
\ No newline at end of file diff --git a/src/main/resources/templates/html/site.html b/src/main/resources/templates/html/site.html new file mode 100644 index 0000000..af7ae37 --- /dev/null +++ b/src/main/resources/templates/html/site.html @@ -0,0 +1,13 @@ + + + + + + %{title} + + + + + %{content} + + \ No newline at end of file diff --git a/src/main/resources/templates/html/student_dashboard.html b/src/main/resources/templates/html/student_dashboard.html new file mode 100644 index 0000000..196edfc --- /dev/null +++ b/src/main/resources/templates/html/student_dashboard.html @@ -0,0 +1,19 @@ +%[site;title=Schüler-Dashboard;content=!FOLLOWS] +
+
+

Schüler:

+

Klasse: | E-Mail: | Graduierung:

+
+
+ + +
+
+

Fächer

+
+
+ +
+ \ No newline at end of file diff --git a/src/main/resources/templates/html/student_results.html b/src/main/resources/templates/html/student_results.html new file mode 100644 index 0000000..661ab57 --- /dev/null +++ b/src/main/resources/templates/html/student_results.html @@ -0,0 +1,23 @@ + + + + + + Ergebnisse – Lernsystem + + + + + +
+
+

Ergebnisse für

+
+
+ + +
+ + \ No newline at end of file diff --git a/src/main/resources/templates/html/subject_info.html b/src/main/resources/templates/html/subject_info.html new file mode 100644 index 0000000..c977813 --- /dev/null +++ b/src/main/resources/templates/html/subject_info.html @@ -0,0 +1,24 @@ +
+

Fächerübersicht

+
+ + + + +
+
+

Schüler in der Klasse:

+ + + + + + + + + + + + +
NameBraucht UnterstützungBraucht Betreuung für ExperimentSucht PartnerIst bereit für den Gelingensnachweis
+
\ No newline at end of file diff --git a/src/test/java/de/igslandstuhl/database/server/webserver/AccessManagerTest.java b/src/test/java/de/igslandstuhl/database/server/webserver/AccessManagerTest.java index 9908305..940bcc4 100644 --- a/src/test/java/de/igslandstuhl/database/server/webserver/AccessManagerTest.java +++ b/src/test/java/de/igslandstuhl/database/server/webserver/AccessManagerTest.java @@ -3,13 +3,16 @@ import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; +import java.io.IOException; import java.sql.SQLException; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import de.igslandstuhl.database.api.User; import de.igslandstuhl.database.server.resources.ResourceLocation; +import de.igslandstuhl.database.server.webserver.handlers.WebResourceHandler; public class AccessManagerTest { private User teacher; @@ -17,6 +20,11 @@ public class AccessManagerTest { private User admin; private User anonymous; + @BeforeAll + public static void setup() throws IOException { + WebPath.registerPaths(); + } + @BeforeEach public void setupUsers() { admin = new User() { @@ -113,44 +121,44 @@ public String getUsername() { } @Test public void testErrorMessageAccess() { - assertTrue(AccessManager.hasAccess(anonymous, ResourceLocation.get("html", "error:404"))); - assertTrue(AccessManager.hasAccess(student, ResourceLocation.get("html", "error:404"))); - assertTrue(AccessManager.hasAccess(teacher, ResourceLocation.get("html", "error:404"))); - assertTrue(AccessManager.hasAccess(admin, ResourceLocation.get("html", "error:404"))); + assertTrue(AccessManager.getInstance().hasAccess(anonymous, ResourceLocation.get("html", "error:404"))); + assertTrue(AccessManager.getInstance().hasAccess(student, ResourceLocation.get("html", "error:404"))); + assertTrue(AccessManager.getInstance().hasAccess(teacher, ResourceLocation.get("html", "error:404"))); + assertTrue(AccessManager.getInstance().hasAccess(admin, ResourceLocation.get("html", "error:404"))); } @Test public void testIconAccess() { - assertTrue(AccessManager.hasAccess(anonymous, ResourceLocation.get("imgs", "icons:favicon.ico"))); - assertTrue(AccessManager.hasAccess(student, ResourceLocation.get("imgs", "icons:favicon.ico"))); - assertTrue(AccessManager.hasAccess(teacher, ResourceLocation.get("imgs", "icons:favicon.ico"))); - assertTrue(AccessManager.hasAccess(admin, ResourceLocation.get("imgs", "icons:favicon.ico"))); + assertTrue(AccessManager.getInstance().hasAccess(anonymous, ResourceLocation.get("imgs", "icons:favicon.ico"))); + assertTrue(AccessManager.getInstance().hasAccess(student, ResourceLocation.get("imgs", "icons:favicon.ico"))); + assertTrue(AccessManager.getInstance().hasAccess(teacher, ResourceLocation.get("imgs", "icons:favicon.ico"))); + assertTrue(AccessManager.getInstance().hasAccess(admin, ResourceLocation.get("imgs", "icons:favicon.ico"))); } @Test public void testLoginAccess() { - assertTrue(AccessManager.hasAccess(anonymous, ResourceLocation.get("html", "site:login"))); - assertTrue(AccessManager.hasAccess(student, ResourceLocation.get("html", "site:login"))); - assertTrue(AccessManager.hasAccess(teacher, ResourceLocation.get("html", "site:login"))); - assertTrue(AccessManager.hasAccess(admin, ResourceLocation.get("html", "site:login"))); + assertTrue(AccessManager.getInstance().hasAccess(anonymous, ResourceLocation.get("html", "site:login"))); + assertTrue(AccessManager.getInstance().hasAccess(student, ResourceLocation.get("html", "site:login"))); + assertTrue(AccessManager.getInstance().hasAccess(teacher, ResourceLocation.get("html", "site:login"))); + assertTrue(AccessManager.getInstance().hasAccess(admin, ResourceLocation.get("html", "site:login"))); } @Test public void testDashboardAccess() { - assertFalse(AccessManager.hasAccess(anonymous, WebResourceHandler.locationFromPath("/dashboard", anonymous))); - assertTrue(AccessManager.hasAccess(student, WebResourceHandler.locationFromPath("/dashboard", student))); - assertTrue(AccessManager.hasAccess(teacher, WebResourceHandler.locationFromPath("/dashboard", teacher))); - assertTrue(AccessManager.hasAccess(admin, WebResourceHandler.locationFromPath("/dashboard", admin))); + assertFalse(AccessManager.getInstance().hasAccess(anonymous, WebResourceHandler.locationFromPath("/dashboard", anonymous))); + assertTrue(AccessManager.getInstance().hasAccess(student, WebResourceHandler.locationFromPath("/dashboard", student))); + assertTrue(AccessManager.getInstance().hasAccess(teacher, WebResourceHandler.locationFromPath("/dashboard", teacher))); + assertTrue(AccessManager.getInstance().hasAccess(admin, WebResourceHandler.locationFromPath("/dashboard", admin))); } @Test public void testStudentManagementAccess() { - assertFalse(AccessManager.hasAccess(anonymous, WebResourceHandler.locationFromPath("/student", anonymous))); - assertFalse(AccessManager.hasAccess(student, WebResourceHandler.locationFromPath("/student", student))); - assertTrue(AccessManager.hasAccess(teacher, WebResourceHandler.locationFromPath("/student", teacher))); - assertTrue(AccessManager.hasAccess(admin, WebResourceHandler.locationFromPath("/student", admin))); + assertFalse(AccessManager.getInstance().hasAccess(anonymous, WebResourceHandler.locationFromPath("/student", anonymous))); + assertFalse(AccessManager.getInstance().hasAccess(student, WebResourceHandler.locationFromPath("/student", student))); + assertTrue(AccessManager.getInstance().hasAccess(teacher, WebResourceHandler.locationFromPath("/student", teacher))); + assertTrue(AccessManager.getInstance().hasAccess(admin, WebResourceHandler.locationFromPath("/student", admin))); } @Test public void testTeacherManagementAccess() { - assertFalse(AccessManager.hasAccess(anonymous, WebResourceHandler.locationFromPath("/teacher", anonymous))); - assertFalse(AccessManager.hasAccess(student, WebResourceHandler.locationFromPath("/teacher", student))); - assertFalse(AccessManager.hasAccess(teacher, WebResourceHandler.locationFromPath("/teacher", teacher))); - assertTrue(AccessManager.hasAccess(admin, WebResourceHandler.locationFromPath("/teacher", admin))); + assertFalse(AccessManager.getInstance().hasAccess(anonymous, WebResourceHandler.locationFromPath("/teacher", anonymous))); + assertFalse(AccessManager.getInstance().hasAccess(student, WebResourceHandler.locationFromPath("/teacher", student))); + assertFalse(AccessManager.getInstance().hasAccess(teacher, WebResourceHandler.locationFromPath("/teacher", teacher))); + assertTrue(AccessManager.getInstance().hasAccess(admin, WebResourceHandler.locationFromPath("/teacher", admin))); } } diff --git a/src/test/java/de/igslandstuhl/database/server/webserver/ContentTypeTest.java b/src/test/java/de/igslandstuhl/database/server/webserver/ContentTypeTest.java index e9a4458..525f056 100644 --- a/src/test/java/de/igslandstuhl/database/server/webserver/ContentTypeTest.java +++ b/src/test/java/de/igslandstuhl/database/server/webserver/ContentTypeTest.java @@ -9,6 +9,7 @@ import de.igslandstuhl.database.api.User; import de.igslandstuhl.database.server.resources.ResourceLocation; +import de.igslandstuhl.database.server.webserver.handlers.WebResourceHandler; public class ContentTypeTest { private User student; diff --git a/src/test/java/de/igslandstuhl/database/server/webserver/GetResponseTest.java b/src/test/java/de/igslandstuhl/database/server/webserver/GetResponseTest.java index da9d3fc..544b651 100644 --- a/src/test/java/de/igslandstuhl/database/server/webserver/GetResponseTest.java +++ b/src/test/java/de/igslandstuhl/database/server/webserver/GetResponseTest.java @@ -36,19 +36,19 @@ void testUnauthorized() throws FileNotFoundException { @Test void testGetResource() throws FileNotFoundException { - assertTrue(GetResponse.getResource(request, ResourceLocation.get("html", "site:login.html"), null).getResponseBody().contains("login")); + assertTrue(GetResponse.getResource(request, ResourceLocation.get("html", "site:login.html"), null, false).getResponseBody().contains("login")); } @Test void testGetResponseBody() { - assertNotNull(GetResponse.getResource(request, ResourceLocation.get("html", "site:login.html"), null)); + assertNotNull(GetResponse.getResource(request, ResourceLocation.get("html", "site:login.html"), null, false)); } @Test void testRespond() throws FileNotFoundException { ByteArrayOutputStream testStream = new ByteArrayOutputStream(); PrintStream printWriter = new PrintStream(testStream); - GetResponse response = GetResponse.getResource(request, ResourceLocation.get("html", "site:login.html"), null); + GetResponse response = GetResponse.getResource(request, ResourceLocation.get("html", "site:login.html"), null, false); response.respond(printWriter); String responseString = testStream.toString(); String responseBody = response.getResponseBody(); diff --git a/src/test/java/de/igslandstuhl/database/server/webserver/HttpHeaderTest.java b/src/test/java/de/igslandstuhl/database/server/webserver/HttpHeaderTest.java index fa5b074..69659a8 100644 --- a/src/test/java/de/igslandstuhl/database/server/webserver/HttpHeaderTest.java +++ b/src/test/java/de/igslandstuhl/database/server/webserver/HttpHeaderTest.java @@ -6,6 +6,8 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import de.igslandstuhl.database.server.webserver.requests.HttpHeader; + public class HttpHeaderTest { HttpHeader postHeader; @BeforeEach diff --git a/src/test/java/de/igslandstuhl/database/server/webserver/PostRequestTest.java b/src/test/java/de/igslandstuhl/database/server/webserver/PostRequestTest.java index cc4c146..eb004c9 100644 --- a/src/test/java/de/igslandstuhl/database/server/webserver/PostRequestTest.java +++ b/src/test/java/de/igslandstuhl/database/server/webserver/PostRequestTest.java @@ -13,6 +13,7 @@ import com.google.gson.JsonSyntaxException; +import de.igslandstuhl.database.server.webserver.requests.HttpHeader; import de.igslandstuhl.database.server.webserver.requests.PostRequest; public class PostRequestTest { diff --git a/src/test/java/de/igslandstuhl/database/server/webserver/SessionManagerTest.java b/src/test/java/de/igslandstuhl/database/server/webserver/SessionManagerTest.java index 7530b2d..659fc5f 100644 --- a/src/test/java/de/igslandstuhl/database/server/webserver/SessionManagerTest.java +++ b/src/test/java/de/igslandstuhl/database/server/webserver/SessionManagerTest.java @@ -13,6 +13,8 @@ import de.igslandstuhl.database.api.PreConditions; import de.igslandstuhl.database.server.webserver.requests.PostRequest; +import de.igslandstuhl.database.server.webserver.sessions.Session; +import de.igslandstuhl.database.server.webserver.sessions.SessionManager; public class SessionManagerTest { private static final String LOCALHOST = "127.0.0.1";