├── .dockerignore ├── .gitignore ├── Dockerfile ├── LICENSE ├── README.md ├── cookies ├── Dockerfile ├── README.md ├── api │ ├── .gitignore │ └── Java_api.java ├── cookies.sh └── hamcrest-all-1.3.jar ├── docker-compose.yml ├── fastapi ├── .gitignore ├── Dockerfile ├── __init__.py ├── alembic │ ├── README │ ├── env.py │ ├── script.py.mako │ └── versions │ │ ├── 075dad69ab65_added_must_change_password_to_user.py │ │ ├── 48e0ab5941fc_init.py │ │ ├── f031d57aa4e6_added_main_class_to_project.py │ │ └── f42eca714ce0_added_enable_tests_to_homework.py ├── alembic_dev.ini ├── alembic_prod.ini ├── api.py ├── auth.py ├── code.py ├── config.py ├── cookies_api.py ├── crud.py ├── database_config.py ├── git.py ├── gunicorn_conf.py ├── java_helloWorld │ ├── de │ │ ├── Schoco.java │ │ └── Tests.java │ └── en │ │ ├── Schoco.java │ │ └── Tests.java ├── main.py ├── models_and_schemas.py ├── prestart.sh ├── requirements.txt └── users.py ├── frontend ├── .env.development ├── .env.production ├── .gitignore ├── README.md ├── index.html ├── package-lock.json ├── package.json ├── public │ ├── favicon.ico │ ├── schoco-icon.svg │ ├── schoco-text-1.svg │ ├── schoco-text-2.svg │ └── schoco-text-3.svg ├── src │ ├── App.vue │ ├── components │ │ ├── ColorModeSwitch.vue │ │ ├── CourseBadge.vue │ │ ├── IDEFileTree.vue │ │ ├── NavBar.vue │ │ ├── PasswordInfo.vue │ │ ├── PasswordInput.vue │ │ ├── ProjectCard.vue │ │ ├── Submission.vue │ │ └── TreeNode.vue │ ├── fonts │ │ └── NotoSans-Regular.ttf │ ├── locales │ │ ├── de │ │ │ ├── color_mode_switch.js │ │ │ ├── general.js │ │ │ ├── home.js │ │ │ ├── homework.js │ │ │ ├── ide.js │ │ │ ├── login.js │ │ │ ├── navbar.js │ │ │ ├── new_project.js │ │ │ ├── password_info.js │ │ │ ├── project_card.js │ │ │ └── users.js │ │ ├── en │ │ │ ├── color_mode_switch.js │ │ │ ├── general.js │ │ │ ├── home.js │ │ │ ├── homework.js │ │ │ ├── ide.js │ │ │ ├── login.js │ │ │ ├── navbar.js │ │ │ ├── new_project.js │ │ │ ├── password_info.js │ │ │ ├── project_card.js │ │ │ └── users.js │ │ └── index.js │ ├── main.js │ ├── router │ │ └── index.js │ ├── sass │ │ └── main.scss │ ├── services │ │ ├── auth-headers.js │ │ ├── code.service.js │ │ ├── user.service.js │ │ └── websocket-worker.js │ ├── stores │ │ └── auth.store.js │ └── views │ │ ├── ChangePassword.vue │ │ ├── Home.vue │ │ ├── Homework.vue │ │ ├── IDE.vue │ │ ├── Login.vue │ │ ├── NewProject.vue │ │ └── Users.vue └── vite.config.js ├── gitea ├── .gitignore └── docker-compose.yml ├── logos ├── favicon.ico ├── favicon.png ├── favicon.svg ├── schoco-full.svg ├── schoco-logo.svg ├── schoco-text-1.svg ├── schoco-text-2.svg ├── schoco-text-3.svg └── schoco-text.svg ├── nginx ├── nginx.conf └── nginx.dev.conf └── readme ├── IDE.png ├── assignment_teacher.png ├── home_assignment_teacher.png ├── schoco-full.png ├── schoco-logo.svg ├── schoco-promo.jpg ├── schoco_architecture.svg ├── usermanagement_course.jpg ├── usermanagement_end.jpg └── usermanagement_students.jpg /.dockerignore: -------------------------------------------------------------------------------- 1 | gitea 2 | cookies 3 | fastapi 4 | data 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | data 2 | *.envrc 3 | .DS_Store -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Dockerfile for the FRONTEND 2 | 3 | # build vue 4 | FROM node:18.20.3-alpine3.20 as build-vue 5 | WORKDIR /app 6 | COPY ./frontend/package*.json . 7 | RUN npm install 8 | COPY ./frontend . 9 | RUN npm run build 10 | 11 | 12 | FROM nginxinc/nginx-unprivileged:1.25.5-alpine3.19 13 | COPY nginx/nginx.conf /etc/nginx/conf.d/default.conf 14 | COPY --from=build-vue /app/dist /usr/share/nginx/html 15 | 16 | # Only contains nginx and the frontend. Is the only entrypoint to the schoco-network! 17 | # Used to filter incoming traffic to ONLY (!!!) allow websocket-container-attachment to /var/run/docker.sock. -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Marco Kümmel 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /cookies/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine:3.17 2 | 3 | COPY cookies.sh /app/cookies.sh 4 | COPY hamcrest-all-1.3.jar /app/hamcrest.jar 5 | COPY api/*.class / 6 | 7 | RUN apk add --no-cache openjdk8 junit libc6-compat bash \ 8 | && chmod +x /app/cookies.sh 9 | 10 | # The Entrypoint starts the minimum-API written in Java. 11 | # The container just waits for commands via the API. 12 | # When a /compile /run /test command is coming in, 13 | # then the cookies.sh-Script gets executed 14 | ENTRYPOINT ["java", "Java_api"] 15 | -------------------------------------------------------------------------------- /cookies/README.md: -------------------------------------------------------------------------------- 1 | COOKIES stands for **Co**mpile **o**nline, **k**eep **i**ts **e**xecution **s**upervised 2 | 3 | Cookies is the docker-image, which will run in multiple instances and do the actual Java compilation and execution work of schoco. For each compilation and execution there's a new container-instance used (TODO: perhaps we can keep the same containers running at least for the compilation!!). Cookies itself is hardly useful and is tighlty coupled to schoco. 4 | 5 | It is based on [codeboard-mantra](https://github.com/codeboardio/mantra), but works slightly different concerning the creation, starting and stopping of the containers: 6 | 7 | - Create and start at least one container to keep it fully ready to start working. Creating and starting takes roughly around 0.3 seconds. We can save this time for each single compilation/execution when we do not wait with starting a container until a new command is coming in. Instead the container is already running, gets prepared for the specific command, and then the command gets executed (using the API inside the container - written in Java, see /api). -------------------------------------------------------------------------------- /cookies/api/.gitignore: -------------------------------------------------------------------------------- 1 | *.class -------------------------------------------------------------------------------- /cookies/api/Java_api.java: -------------------------------------------------------------------------------- 1 | import com.sun.net.httpserver.HttpServer; 2 | import com.sun.net.httpserver.HttpExchange; 3 | import com.sun.net.httpserver.HttpHandler; 4 | import java.io.IOException; 5 | import java.io.InputStream; 6 | import java.io.OutputStream; 7 | import java.io.BufferedReader; 8 | import java.io.InputStreamReader; 9 | import java.net.InetSocketAddress; 10 | import java.util.HashMap; 11 | import java.util.stream.Collectors; 12 | 13 | public class Java_api { 14 | public static void main(String[] args) throws IOException, InterruptedException { 15 | Java_api server = new Java_api(); 16 | } 17 | 18 | public Java_api() throws IOException { 19 | HttpServer server = HttpServer.create(new InetSocketAddress(8080), 0); 20 | server.createContext("/compile", new HttpHandler() { 21 | 22 | @Override 23 | public void handle(HttpExchange exchange) throws IOException { 24 | if ("POST".equals(exchange.getRequestMethod())) { 25 | 26 | HashMap postData = getRequestData(exchange.getRequestBody()); 27 | 28 | int exitCode = 0; 29 | String stdout = ""; 30 | 31 | try { 32 | String[] command = { "sh", "-c", "bash /app/cookies.sh 'javac -cp /app/tmp/:/usr/share/java/junit.jar /app/tmp/*.java' " 33 | + postData.get("timeout_cpu") 34 | + " " 35 | + postData.get("timeout_session") 36 | + (postData.get("save_output").trim().equals("true") ? " ; echo '\nschoco compilation finished SBsodjpFo43E5Y7d'" : "") 37 | + "; exit" }; // random string to indicate end of execution 38 | 39 | Process p; 40 | 41 | if (postData.get("save_output").trim().equals("true")) { 42 | p = new ProcessBuilder().redirectErrorStream(true).command(command).start(); 43 | 44 | BufferedReader reader = 45 | new BufferedReader(new InputStreamReader(p.getInputStream())); 46 | StringBuilder builder = new StringBuilder(); 47 | String line = null; 48 | 49 | while ((line = reader.readLine()) != null) { 50 | if (line.equals("schoco compilation finished SBsodjpFo43E5Y7d")) break; 51 | builder.append(line); 52 | builder.append(System.getProperty("line.separator")); 53 | } 54 | 55 | stdout = builder.toString(); 56 | } else { 57 | p = new ProcessBuilder().inheritIO().command(command).start(); 58 | } 59 | 60 | exitCode = p.waitFor(); 61 | System.out.flush(); 62 | 63 | } catch (InterruptedException e) { 64 | e.printStackTrace(); 65 | exchange.sendResponseHeaders(500, -1);// Internal Server Error 66 | OutputStream os = exchange.getResponseBody(); 67 | os.write("InterruptedException occured".getBytes()); 68 | os.close(); 69 | return; 70 | } catch (Exception e) { 71 | e.printStackTrace(); 72 | exchange.sendResponseHeaders(501, -1); 73 | OutputStream os = exchange.getResponseBody(); 74 | os.write("Exception occured".getBytes()); 75 | os.close(); 76 | return; 77 | } 78 | 79 | String responseText = "{\"exitCode\":\"" + exitCode + (postData.get("save_output").trim().equals("true") ? ("\", \"stdout\":\"" + stdout.replaceAll("\"","\\\\\"")) : "") + "\"}"; 80 | exchange.sendResponseHeaders(200, responseText.getBytes().length); 81 | OutputStream os = exchange.getResponseBody(); 82 | os.write(responseText.getBytes()); 83 | os.close(); 84 | } else { 85 | exchange.sendResponseHeaders(405, -1);// 405 Method Not Allowed 86 | } 87 | exchange.close(); 88 | } 89 | }); 90 | 91 | server.createContext("/execute", new HttpHandler() { 92 | 93 | @Override 94 | public void handle(HttpExchange exchange) throws IOException { 95 | if ("POST".equals(exchange.getRequestMethod())) { 96 | 97 | HashMap postData = getRequestData(exchange.getRequestBody()); 98 | 99 | int exitCode = 0; 100 | String stdout = ""; 101 | 102 | try { 103 | String[] command = { "sh", "-c", "bash /app/cookies.sh 'java -Djava.security.manager=default -cp /app/tmp " 104 | + postData.get("entry_point") 105 | + "' " 106 | + postData.get("timeout_cpu") 107 | + " " 108 | + postData.get("timeout_session") 109 | + (postData.get("save_output").trim().equals("true") ? " ; echo '\nschoco execution finished JVXjUq5wpdxDTki5'" : "") 110 | + "; exit" }; 111 | 112 | Process p; 113 | 114 | if (postData.get("save_output").trim().equals("true")) { 115 | p = new ProcessBuilder().redirectErrorStream(true).command(command).start(); 116 | 117 | BufferedReader reader = 118 | new BufferedReader(new InputStreamReader(p.getInputStream())); 119 | StringBuilder builder = new StringBuilder(); 120 | String line = null; 121 | 122 | while ((line = reader.readLine()) != null) { 123 | if (line.equals("schoco execution finished JVXjUq5wpdxDTki5")) break; 124 | builder.append(line); 125 | builder.append(System.getProperty("line.separator")); 126 | } 127 | 128 | stdout = builder.toString(); 129 | } else { 130 | p = new ProcessBuilder().inheritIO().command(command).start(); 131 | } 132 | 133 | exitCode = p.waitFor(); 134 | System.out.flush(); 135 | 136 | } catch (InterruptedException e) { 137 | e.printStackTrace(); 138 | exchange.sendResponseHeaders(500, -1);// Internal Server Error 139 | OutputStream os = exchange.getResponseBody(); 140 | os.write("InterruptedException occured".getBytes()); 141 | os.close(); 142 | return; 143 | } catch (Exception e) { 144 | e.printStackTrace(); 145 | exchange.sendResponseHeaders(501, -1); 146 | OutputStream os = exchange.getResponseBody(); 147 | os.write("Exception occured".getBytes()); 148 | os.close(); 149 | return; 150 | } 151 | 152 | String responseText = "{\"exitCode\":\"" + exitCode + (postData.get("save_output").trim().equals("true") ? ("\", \"stdout\":\"" + stdout.replaceAll("\"","\\\\\"")) : "") + "\"}"; 153 | exchange.sendResponseHeaders(200, responseText.getBytes().length); 154 | OutputStream os = exchange.getResponseBody(); 155 | os.write(responseText.getBytes()); 156 | os.close(); 157 | } else { 158 | exchange.sendResponseHeaders(405, -1);// 405 Method Not Allowed 159 | } 160 | exchange.close(); 161 | } 162 | }); 163 | 164 | server.createContext("/test", new HttpHandler() { 165 | 166 | @Override 167 | public void handle(HttpExchange exchange) throws IOException { 168 | if ("POST".equals(exchange.getRequestMethod())) { 169 | 170 | HashMap postData = getRequestData(exchange.getRequestBody()); 171 | 172 | int exitCode = 0; 173 | String stdout; 174 | 175 | try { 176 | String[] command = { "sh", "-c", "bash /app/cookies.sh 'java -cp /app/tmp:/usr/share/java/junit.jar:/app/hamcrest.jar org.junit.runner.JUnitCore Tests' " + postData.get("timeout_cpu") + " " + postData.get("timeout_session") + " ; echo '\nschoco JUnit finished'; exit" }; 177 | Process p = new ProcessBuilder().redirectErrorStream(true).command(command).start(); 178 | 179 | BufferedReader reader = 180 | new BufferedReader(new InputStreamReader(p.getInputStream())); 181 | StringBuilder builder = new StringBuilder(); 182 | String line = null; 183 | 184 | while ((line = reader.readLine()) != null) { 185 | if (line.equals("schoco JUnit finished")) break; 186 | //System.out.println(line); 187 | //System.out.flush(); 188 | builder.append(line); 189 | builder.append(System.getProperty("line.separator")); 190 | } 191 | 192 | stdout = builder.toString(); 193 | 194 | exitCode = p.waitFor(); 195 | System.out.flush(); 196 | 197 | } catch (InterruptedException e) { 198 | e.printStackTrace(); 199 | exchange.sendResponseHeaders(500, -1);// Internal Server Error 200 | OutputStream os = exchange.getResponseBody(); 201 | os.write("InterruptedException occured".getBytes()); 202 | os.close(); 203 | return; 204 | } catch (Exception e) { 205 | e.printStackTrace(); 206 | exchange.sendResponseHeaders(501, -1); 207 | OutputStream os = exchange.getResponseBody(); 208 | os.write("Exception occured".getBytes()); 209 | os.close(); 210 | return; 211 | } 212 | 213 | String responseText = "{\"exitCode\":\"" + exitCode + "\", \"stdout\":\"" + stdout + "\"}"; 214 | exchange.sendResponseHeaders(200, responseText.getBytes().length); 215 | OutputStream os = exchange.getResponseBody(); 216 | os.write(responseText.getBytes()); 217 | os.close(); 218 | } else { 219 | exchange.sendResponseHeaders(405, -1);// 405 Method Not Allowed 220 | } 221 | exchange.close(); 222 | } 223 | }); 224 | 225 | server.setExecutor(null); // creates a default executor 226 | server.start(); 227 | } 228 | 229 | 230 | 231 | private HashMap getRequestData(InputStream is) throws IOException { 232 | HashMap request = new HashMap<>(); 233 | 234 | StringBuilder sb = new StringBuilder(); 235 | int i; 236 | while ((i = is.read()) != -1) { 237 | sb.append((char) i); 238 | } 239 | String rs = sb.toString(); 240 | if (rs.startsWith("'{") || rs.startsWith("\"{")) 241 | rs = rs.substring(2, rs.length()-2); 242 | else if (rs.startsWith("{")) 243 | rs = rs.substring(1, rs.length()-1); 244 | for (String kv : rs.split(",")) { 245 | kv = kv.trim(); 246 | String key = kv.split(":")[0]; 247 | if (key.startsWith("\"") || key.startsWith("\'")) 248 | key = key.substring(1, key.length()-1); 249 | String value = kv.split(":")[1]; 250 | if (value.startsWith("\"") || value.startsWith("\'")) 251 | value = value.substring(1, value.length()-1); 252 | request.put(key, value); 253 | } 254 | return request; 255 | } 256 | } -------------------------------------------------------------------------------- /cookies/cookies.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ### 4 | # Schoco-cookies script 5 | # (based on Codeboard-Mantra, see below) 6 | #----------------------------- 7 | # 8 | # Codeboard - Mantra script 9 | # 10 | # Executes "compile" or "run" actions 11 | # Times out when either a CPU usage limit is reached 12 | # or the process (session) has been alive for a certain time 13 | # 14 | # IMPORTANT: this script assumes that you have 'bc' installed (apt-get install bc) 15 | # Note: we run this in a bash because bash has the "time" command 16 | # 17 | # Copyright: H.-Christian Estler 18 | # 19 | ### 20 | 21 | CMD_ARG=$1 22 | TIMEOUT_CPU=$2 23 | TIMEOUT_SESSION=$3 24 | 25 | ( sleep $TIMEOUT_SESSION && echo -e "\n\nSchoco terminated your program because it exceeded the session time limit.\n" && kill $$ ) & 26 | 27 | forkedPID=$! 28 | disown 29 | 30 | ulimit -t $TIMEOUT_CPU 31 | exec 3>&1 4>&2; 32 | CPU_TIME_STRING=$(TIMEFORMAT="%U;%S"; { time sh -c "$CMD_ARG" 1>&3 2>&4; } 2>&1); 33 | CPU_TIME_USER=$(echo $CPU_TIME_STRING | cut -d ';' -f 1) 34 | CPU_TIME_SYSTEM=$(echo $CPU_TIME_STRING | cut -d ';' -f 2) 35 | exec 3>&- 4>&-; 36 | 37 | if [ $(echo "$CPU_TIME_USER + $CPU_TIME_SYSTEM + 0.1 >= $TIMEOUT_CPU" | bc) -eq 1 ]; then 38 | echo -e "\n\nSchoco terminated your program because it exceeded the CPU usage limit.\n" 39 | fi; 40 | 41 | kill $forkedPID 42 | -------------------------------------------------------------------------------- /cookies/hamcrest-all-1.3.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PhiTux/schoco/c302238b5c42b8125eff5d3bfd2d4a2485f14f69/cookies/hamcrest-all-1.3.jar -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | networks: 2 | schoco: 3 | name: schoco 4 | 5 | services: 6 | schoco-backend: 7 | image: phitux/schoco-backend: # use the newest tag, see https://hub.docker.com/r/phitux/schoco-backend/tags 8 | container_name: schoco-backend 9 | restart: always 10 | user: "1000:1000" # find out the user-id (uid) and group-id (gid) of the new user schoco by running 'id schoco' in your bash 11 | group_add: 12 | # find your docker group ID. Either run in your bash: export DOCKER_GROUP_ID=$(getent group docker | cut -d: -f3) 13 | # and import it as variable, or just run the command from within the brackets and replace ${DOCKER_GROUP_ID} with the output. 14 | # Important: the group ID must be used as String (in quotes)! 15 | - ${DOCKER_GROUP_ID} 16 | environment: 17 | - FULL_DATA_PATH=/path/to/my/data 18 | # same as (left part of) first volume - but here as FULL PATH!!! 19 | 20 | - MAX_CONTAINERS=4 21 | # sets the amount of java-workers (you want to set this higher!) I recommend as rule of thumb 1.5x the amout of cores of your CPU 22 | 23 | - SECRET_KEY=secret 24 | # used for session token 25 | 26 | - TEACHER_KEY=teacherkey 27 | # this is the 'password' that is used to create new teacher-accounts. It must only be known to the teachers. 28 | 29 | - GITEA_USERNAME=schoco 30 | # this is the username of the gitea-user (see last image in this yaml-file) 31 | 32 | - GITEA_PASSWORD=schoco1234 33 | # and that is the password of the gitea-user. 34 | # Actually both username and password can stay like this, if you use the gitea-image from this yaml-file and if gitea is not made public (default)! 35 | 36 | - GITEA_HOST=http://schoco-gitea:3000 37 | # stays like this, if you use the gitea-image from this yaml-file and if gitea is not made public (default)! 38 | # change it to your domain, if you use a public gitea-instance 39 | networks: 40 | - schoco 41 | volumes: 42 | - ./data:/app/data 43 | - /var/run/docker.sock:/var/run/docker.sock 44 | 45 | schoco-frontend: 46 | image: phitux/schoco-frontend: # always use the same tag as schoco-backend (see https://hub.docker.com/r/phitux/schoco-frontend/tags) 47 | container_name: schoco-frontend 48 | restart: always 49 | group_add: 50 | - ${DOCKER_GROUP_ID} # see above 51 | networks: 52 | - schoco 53 | volumes: 54 | - /var/run/docker.sock:/var/run/docker.sock 55 | ports: 56 | - "80:8080" # adapt the left host-port to your needs 57 | 58 | schoco-gitea: 59 | image: gitea/gitea:1.17.3 # you could probably use a newer version, but API-changes might break something... 60 | container_name: schoco-gitea 61 | restart: always 62 | environment: 63 | - USER_UID=1000 64 | - USER_GID=1000 65 | - GITEA__security__INSTALL_LOCK=true 66 | networks: 67 | - schoco 68 | volumes: 69 | - ./gitea-data:/data 70 | - /etc/timezone:/etc/timezone:ro 71 | - /etc/localtime:/etc/localtime:ro 72 | -------------------------------------------------------------------------------- /fastapi/.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__/ 2 | sql_app.db 3 | data/* 4 | *.class 5 | .envrc* 6 | ..envrc.un~ 7 | .venv 8 | venv 9 | -------------------------------------------------------------------------------- /fastapi/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM tiangolo/uvicorn-gunicorn-fastapi:python3.10-slim 2 | COPY ./requirements.txt /app/requirements.txt 3 | RUN apt-get update && \ 4 | apt-get install -y libcurl4-openssl-dev libssl-dev gcc && \ 5 | pip install --no-cache-dir --upgrade -r /app/requirements.txt && \ 6 | apt-get purge -y gcc && \ 7 | apt-get autoremove -y && \ 8 | rm -rf /var/lib/apt/lists/* 9 | COPY . /app 10 | -------------------------------------------------------------------------------- /fastapi/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PhiTux/schoco/c302238b5c42b8125eff5d3bfd2d4a2485f14f69/fastapi/__init__.py -------------------------------------------------------------------------------- /fastapi/alembic/README: -------------------------------------------------------------------------------- 1 | Generic single-database configuration. -------------------------------------------------------------------------------- /fastapi/alembic/env.py: -------------------------------------------------------------------------------- 1 | from logging.config import fileConfig 2 | 3 | from sqlalchemy import engine_from_config 4 | from sqlalchemy import pool 5 | from sqlmodel import SQLModel 6 | 7 | # this line has to stay, otherwise the models won't be found (although the import seems not to be used!) 8 | from models_and_schemas import UserCourseLink, User, Course, Project, Homework, EditingHomework 9 | 10 | from alembic import context 11 | 12 | # this is the Alembic Config object, which provides 13 | # access to the values within the .ini file in use. 14 | config = context.config 15 | 16 | # Interpret the config file for Python logging. 17 | # This line sets up loggers basically. 18 | if config.config_file_name is not None: 19 | fileConfig(config.config_file_name) 20 | 21 | # add your model's MetaData object here 22 | # for 'autogenerate' support 23 | # from myapp import mymodel 24 | # target_metadata = mymodel.Base.metadata 25 | target_metadata = SQLModel.metadata 26 | 27 | # other values from the config, defined by the needs of env.py, 28 | # can be acquired: 29 | # my_important_option = config.get_main_option("my_important_option") 30 | # ... etc. 31 | 32 | 33 | def run_migrations_offline() -> None: 34 | """Run migrations in 'offline' mode. 35 | 36 | This configures the context with just a URL 37 | and not an Engine, though an Engine is acceptable 38 | here as well. By skipping the Engine creation 39 | we don't even need a DBAPI to be available. 40 | 41 | Calls to context.execute() here emit the given string to the 42 | script output. 43 | 44 | """ 45 | url = config.get_main_option("sqlalchemy.url") 46 | context.configure( 47 | url=url, 48 | target_metadata=target_metadata, 49 | literal_binds=True, 50 | dialect_opts={"paramstyle": "named"}, 51 | ) 52 | 53 | with context.begin_transaction(): 54 | context.run_migrations() 55 | 56 | 57 | def run_migrations_online() -> None: 58 | """Run migrations in 'online' mode. 59 | 60 | In this scenario we need to create an Engine 61 | and associate a connection with the context. 62 | 63 | """ 64 | connectable = engine_from_config( 65 | config.get_section(config.config_ini_section, {}), 66 | prefix="sqlalchemy.", 67 | poolclass=pool.NullPool, 68 | ) 69 | 70 | with connectable.connect() as connection: 71 | context.configure( 72 | connection=connection, target_metadata=target_metadata 73 | ) 74 | 75 | with context.begin_transaction(): 76 | context.run_migrations() 77 | 78 | 79 | if context.is_offline_mode(): 80 | run_migrations_offline() 81 | else: 82 | run_migrations_online() 83 | -------------------------------------------------------------------------------- /fastapi/alembic/script.py.mako: -------------------------------------------------------------------------------- 1 | """${message} 2 | 3 | Revision ID: ${up_revision} 4 | Revises: ${down_revision | comma,n} 5 | Create Date: ${create_date} 6 | 7 | """ 8 | from typing import Sequence, Union 9 | import sqlmodel 10 | 11 | from alembic import op 12 | import sqlalchemy as sa 13 | ${imports if imports else ""} 14 | 15 | # revision identifiers, used by Alembic. 16 | revision: str = ${repr(up_revision)} 17 | down_revision: Union[str, None] = ${repr(down_revision)} 18 | branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)} 19 | depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)} 20 | 21 | 22 | def upgrade() -> None: 23 | ${upgrades if upgrades else "pass"} 24 | 25 | 26 | def downgrade() -> None: 27 | ${downgrades if downgrades else "pass"} 28 | -------------------------------------------------------------------------------- /fastapi/alembic/versions/075dad69ab65_added_must_change_password_to_user.py: -------------------------------------------------------------------------------- 1 | """added must_change_password to user 2 | 3 | Revision ID: 075dad69ab65 4 | Revises: f42eca714ce0 5 | Create Date: 2024-01-03 15:24:45.889915 6 | 7 | """ 8 | from typing import Sequence, Union 9 | import sqlmodel 10 | 11 | from alembic import op 12 | from sqlalchemy.sql import false 13 | import sqlalchemy as sa 14 | 15 | 16 | # revision identifiers, used by Alembic. 17 | revision: str = '075dad69ab65' 18 | down_revision: Union[str, None] = 'f42eca714ce0' 19 | branch_labels: Union[str, Sequence[str], None] = None 20 | depends_on: Union[str, Sequence[str], None] = None 21 | 22 | 23 | def upgrade() -> None: 24 | # ### commands auto generated by Alembic - please adjust! ### 25 | op.add_column('user', sa.Column('must_change_password', sa.Boolean(), nullable=True, server_default=false())) 26 | # ### end Alembic commands ### 27 | 28 | 29 | def downgrade() -> None: 30 | # ### commands auto generated by Alembic - please adjust! ### 31 | op.drop_column('user', 'must_change_password') 32 | # ### end Alembic commands ### 33 | -------------------------------------------------------------------------------- /fastapi/alembic/versions/48e0ab5941fc_init.py: -------------------------------------------------------------------------------- 1 | """init 2 | 3 | Revision ID: 48e0ab5941fc 4 | Revises: 5 | Create Date: 2023-10-16 18:22:37.026689 6 | 7 | """ 8 | from typing import Sequence, Union 9 | import sqlmodel 10 | 11 | from alembic import op 12 | import sqlalchemy as sa 13 | 14 | 15 | # revision identifiers, used by Alembic. 16 | revision: str = '48e0ab5941fc' 17 | down_revision: Union[str, None] = None 18 | branch_labels: Union[str, Sequence[str], None] = None 19 | depends_on: Union[str, Sequence[str], None] = None 20 | 21 | 22 | def upgrade() -> None: 23 | # ### commands auto generated by Alembic - please adjust! ### 24 | op.create_table('course', 25 | sa.Column('id', sa.Integer(), nullable=False), 26 | sa.Column( 27 | 'name', sqlmodel.sql.sqltypes.AutoString(), nullable=False), 28 | sa.Column( 29 | 'color', sqlmodel.sql.sqltypes.AutoString(), nullable=False), 30 | sa.Column('fontDark', sa.Boolean(), nullable=False), 31 | sa.PrimaryKeyConstraint('id'), 32 | sa.UniqueConstraint('name') 33 | ) 34 | op.create_table('user', 35 | sa.Column( 36 | 'username', sqlmodel.sql.sqltypes.AutoString(), nullable=False), 37 | sa.Column( 38 | 'full_name', sqlmodel.sql.sqltypes.AutoString(), nullable=False), 39 | sa.Column( 40 | 'role', sqlmodel.sql.sqltypes.AutoString(), nullable=False), 41 | sa.Column('id', sa.Integer(), nullable=False), 42 | sa.Column('hashed_password', 43 | sqlmodel.sql.sqltypes.AutoString(), nullable=False), 44 | sa.PrimaryKeyConstraint('id'), 45 | sa.UniqueConstraint('username') 46 | ) 47 | op.create_table('project', 48 | sa.Column('id', sa.Integer(), nullable=False), 49 | sa.Column( 50 | 'uuid', sqlmodel.sql.sqltypes.AutoString(), nullable=False), 51 | sa.Column( 52 | 'name', sqlmodel.sql.sqltypes.AutoString(), nullable=False), 53 | sa.Column('description', sqlmodel.sql.sqltypes.AutoString(), 54 | nullable=False), 55 | sa.Column('owner_id', sa.Integer(), nullable=False), 56 | sa.Column('computation_time', sa.Integer(), nullable=True), 57 | sa.ForeignKeyConstraint(['owner_id'], ['user.id'], ), 58 | sa.PrimaryKeyConstraint('id'), 59 | sa.UniqueConstraint('uuid') 60 | ) 61 | op.create_table('usercourselink', 62 | sa.Column('user_id', sa.Integer(), nullable=False), 63 | sa.Column('course_id', sa.Integer(), nullable=False), 64 | sa.ForeignKeyConstraint(['course_id'], ['course.id'], ), 65 | sa.ForeignKeyConstraint(['user_id'], ['user.id'], ), 66 | sa.PrimaryKeyConstraint('user_id', 'course_id') 67 | ) 68 | op.create_table('homework', 69 | sa.Column('id', sa.Integer(), nullable=False), 70 | sa.Column('course_id', sa.Integer(), nullable=True), 71 | sa.Column('template_project_id', 72 | sa.Integer(), nullable=True), 73 | sa.Column('original_project_id', 74 | sa.Integer(), nullable=True), 75 | sa.Column('deadline', sqlmodel.sql.sqltypes.AutoString(), 76 | nullable=False), 77 | sa.Column('solution_project_id', 78 | sa.Integer(), nullable=True), 79 | sa.Column('solution_start_showing', 80 | sqlmodel.sql.sqltypes.AutoString(), nullable=True), 81 | sa.ForeignKeyConstraint(['course_id'], ['course.id'], ), 82 | sa.ForeignKeyConstraint( 83 | ['original_project_id'], ['project.id'], ), 84 | sa.ForeignKeyConstraint( 85 | ['solution_project_id'], ['project.id'], ), 86 | sa.ForeignKeyConstraint( 87 | ['template_project_id'], ['project.id'], ), 88 | sa.PrimaryKeyConstraint('id'), 89 | sa.UniqueConstraint('template_project_id') 90 | ) 91 | op.create_table('editinghomework', 92 | sa.Column('id', sa.Integer(), nullable=False), 93 | sa.Column('homework_id', sa.Integer(), nullable=True), 94 | sa.Column('owner_id', sa.Integer(), nullable=True), 95 | sa.Column('submission', sqlmodel.sql.sqltypes.AutoString(), 96 | nullable=True), 97 | sa.Column('number_of_compilations', 98 | sa.Integer(), nullable=True), 99 | sa.Column('number_of_runs', sa.Integer(), nullable=True), 100 | sa.Column('number_of_tests', sa.Integer(), nullable=True), 101 | sa.ForeignKeyConstraint( 102 | ['homework_id'], ['homework.id'], ), 103 | sa.ForeignKeyConstraint(['owner_id'], ['user.id'], ), 104 | sa.PrimaryKeyConstraint('id') 105 | ) 106 | # ### end Alembic commands ### 107 | 108 | 109 | def downgrade() -> None: 110 | # ### commands auto generated by Alembic - please adjust! ### 111 | op.drop_table('editinghomework') 112 | op.drop_table('homework') 113 | op.drop_table('usercourselink') 114 | op.drop_table('project') 115 | op.drop_table('user') 116 | op.drop_table('course') 117 | # ### end Alembic commands ### 118 | -------------------------------------------------------------------------------- /fastapi/alembic/versions/f031d57aa4e6_added_main_class_to_project.py: -------------------------------------------------------------------------------- 1 | """added main_class to project 2 | 3 | Revision ID: f031d57aa4e6 4 | Revises: 48e0ab5941fc 5 | Create Date: 2023-09-30 17:30:06.131495 6 | 7 | """ 8 | from typing import Sequence, Union 9 | import sqlmodel 10 | 11 | from alembic import op 12 | import sqlalchemy as sa 13 | 14 | 15 | # revision identifiers, used by Alembic. 16 | revision: str = 'f031d57aa4e6' 17 | down_revision: Union[str, None] = '48e0ab5941fc' 18 | branch_labels: Union[str, Sequence[str], None] = None 19 | depends_on: Union[str, Sequence[str], None] = None 20 | 21 | 22 | def upgrade() -> None: 23 | # ### commands auto generated by Alembic - please adjust! ### 24 | op.add_column('project', sa.Column('main_class', sqlmodel.sql.sqltypes.AutoString( 25 | ), nullable=False, server_default='Schoco.java/')) 26 | # ### end Alembic commands ### 27 | 28 | 29 | def downgrade() -> None: 30 | # ### commands auto generated by Alembic - please adjust! ### 31 | op.drop_column('project', 'main_class') 32 | # ### end Alembic commands ### 33 | -------------------------------------------------------------------------------- /fastapi/alembic/versions/f42eca714ce0_added_enable_tests_to_homework.py: -------------------------------------------------------------------------------- 1 | """added enable_tests to homework 2 | 3 | Revision ID: f42eca714ce0 4 | Revises: f031d57aa4e6 5 | Create Date: 2023-10-21 16:20:39.538990 6 | 7 | """ 8 | from typing import Sequence, Union 9 | import sqlmodel 10 | 11 | from alembic import op 12 | import sqlalchemy as sa 13 | 14 | 15 | # revision identifiers, used by Alembic. 16 | revision: str = 'f42eca714ce0' 17 | down_revision: Union[str, None] = 'f031d57aa4e6' 18 | branch_labels: Union[str, Sequence[str], None] = None 19 | depends_on: Union[str, Sequence[str], None] = None 20 | 21 | 22 | def upgrade() -> None: 23 | # ### commands auto generated by Alembic - please adjust! ### 24 | op.add_column('homework', sa.Column('enable_tests', sa.Boolean(), nullable=True, default=True)) 25 | # ### end Alembic commands ### 26 | 27 | 28 | def downgrade() -> None: 29 | # ### commands auto generated by Alembic - please adjust! ### 30 | op.drop_column('homework', 'enable_tests') 31 | # ### end Alembic commands ### 32 | -------------------------------------------------------------------------------- /fastapi/alembic_dev.ini: -------------------------------------------------------------------------------- 1 | # A generic, single database configuration. 2 | 3 | [alembic] 4 | # path to migration scripts 5 | script_location = alembic 6 | 7 | # template used to generate migration file names; The default value is %%(rev)s_%%(slug)s 8 | # Uncomment the line below if you want the files to be prepended with date and time 9 | # see https://alembic.sqlalchemy.org/en/latest/tutorial.html#editing-the-ini-file 10 | # for all available tokens 11 | # file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s 12 | 13 | # sys.path path, will be prepended to sys.path if present. 14 | # defaults to the current working directory. 15 | prepend_sys_path = . 16 | 17 | # timezone to use when rendering the date within the migration file 18 | # as well as the filename. 19 | # If specified, requires the python-dateutil library that can be 20 | # installed by adding `alembic[tz]` to the pip requirements 21 | # string value is passed to dateutil.tz.gettz() 22 | # leave blank for localtime 23 | # timezone = 24 | 25 | # max length of characters to apply to the 26 | # "slug" field 27 | # truncate_slug_length = 40 28 | 29 | # set to 'true' to run the environment during 30 | # the 'revision' command, regardless of autogenerate 31 | # revision_environment = false 32 | 33 | # set to 'true' to allow .pyc and .pyo files without 34 | # a source .py file to be detected as revisions in the 35 | # versions/ directory 36 | # sourceless = false 37 | 38 | # version location specification; This defaults 39 | # to alembic/versions. When using multiple version 40 | # directories, initial revisions must be specified with --version-path. 41 | # The path separator used here should be the separator specified by "version_path_separator" below. 42 | # version_locations = %(here)s/bar:%(here)s/bat:alembic/versions 43 | 44 | # version path separator; As mentioned above, this is the character used to split 45 | # version_locations. The default within new alembic.ini files is "os", which uses os.pathsep. 46 | # If this key is omitted entirely, it falls back to the legacy behavior of splitting on spaces and/or commas. 47 | # Valid values for version_path_separator are: 48 | # 49 | # version_path_separator = : 50 | # version_path_separator = ; 51 | # version_path_separator = space 52 | version_path_separator = os # Use os.pathsep. Default configuration used for new projects. 53 | 54 | # set to 'true' to search source files recursively 55 | # in each "version_locations" directory 56 | # new in Alembic version 1.10 57 | # recursive_version_locations = false 58 | 59 | # the output encoding used when revision files 60 | # are written from script.py.mako 61 | # output_encoding = utf-8 62 | 63 | sqlalchemy.url = sqlite:///./data/sql_app.db 64 | 65 | 66 | [post_write_hooks] 67 | # post_write_hooks defines scripts or Python functions that are run 68 | # on newly generated revision scripts. See the documentation for further 69 | # detail and examples 70 | 71 | # format using "black" - use the console_scripts runner, against the "black" entrypoint 72 | # hooks = black 73 | # black.type = console_scripts 74 | # black.entrypoint = black 75 | # black.options = -l 79 REVISION_SCRIPT_FILENAME 76 | 77 | # Logging configuration 78 | [loggers] 79 | keys = root,sqlalchemy,alembic 80 | 81 | [handlers] 82 | keys = console 83 | 84 | [formatters] 85 | keys = generic 86 | 87 | [logger_root] 88 | level = WARN 89 | handlers = console 90 | qualname = 91 | 92 | [logger_sqlalchemy] 93 | level = WARN 94 | handlers = 95 | qualname = sqlalchemy.engine 96 | 97 | [logger_alembic] 98 | level = INFO 99 | handlers = 100 | qualname = alembic 101 | 102 | [handler_console] 103 | class = StreamHandler 104 | args = (sys.stderr,) 105 | level = NOTSET 106 | formatter = generic 107 | 108 | [formatter_generic] 109 | format = %(levelname)-5.5s [%(name)s] %(message)s 110 | datefmt = %H:%M:%S 111 | -------------------------------------------------------------------------------- /fastapi/alembic_prod.ini: -------------------------------------------------------------------------------- 1 | # A generic, single database configuration. 2 | 3 | [alembic] 4 | # path to migration scripts 5 | script_location = alembic 6 | 7 | # template used to generate migration file names; The default value is %%(rev)s_%%(slug)s 8 | # Uncomment the line below if you want the files to be prepended with date and time 9 | # see https://alembic.sqlalchemy.org/en/latest/tutorial.html#editing-the-ini-file 10 | # for all available tokens 11 | # file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s 12 | 13 | # sys.path path, will be prepended to sys.path if present. 14 | # defaults to the current working directory. 15 | prepend_sys_path = . 16 | 17 | # timezone to use when rendering the date within the migration file 18 | # as well as the filename. 19 | # If specified, requires the python-dateutil library that can be 20 | # installed by adding `alembic[tz]` to the pip requirements 21 | # string value is passed to dateutil.tz.gettz() 22 | # leave blank for localtime 23 | # timezone = 24 | 25 | # max length of characters to apply to the 26 | # "slug" field 27 | # truncate_slug_length = 40 28 | 29 | # set to 'true' to run the environment during 30 | # the 'revision' command, regardless of autogenerate 31 | # revision_environment = false 32 | 33 | # set to 'true' to allow .pyc and .pyo files without 34 | # a source .py file to be detected as revisions in the 35 | # versions/ directory 36 | # sourceless = false 37 | 38 | # version location specification; This defaults 39 | # to alembic/versions. When using multiple version 40 | # directories, initial revisions must be specified with --version-path. 41 | # The path separator used here should be the separator specified by "version_path_separator" below. 42 | # version_locations = %(here)s/bar:%(here)s/bat:alembic/versions 43 | 44 | # version path separator; As mentioned above, this is the character used to split 45 | # version_locations. The default within new alembic.ini files is "os", which uses os.pathsep. 46 | # If this key is omitted entirely, it falls back to the legacy behavior of splitting on spaces and/or commas. 47 | # Valid values for version_path_separator are: 48 | # 49 | # version_path_separator = : 50 | # version_path_separator = ; 51 | # version_path_separator = space 52 | version_path_separator = os # Use os.pathsep. Default configuration used for new projects. 53 | 54 | # set to 'true' to search source files recursively 55 | # in each "version_locations" directory 56 | # new in Alembic version 1.10 57 | # recursive_version_locations = false 58 | 59 | # the output encoding used when revision files 60 | # are written from script.py.mako 61 | # output_encoding = utf-8 62 | 63 | sqlalchemy.url = sqlite:////app/data/sql_app.db 64 | 65 | 66 | [post_write_hooks] 67 | # post_write_hooks defines scripts or Python functions that are run 68 | # on newly generated revision scripts. See the documentation for further 69 | # detail and examples 70 | 71 | # format using "black" - use the console_scripts runner, against the "black" entrypoint 72 | # hooks = black 73 | # black.type = console_scripts 74 | # black.entrypoint = black 75 | # black.options = -l 79 REVISION_SCRIPT_FILENAME 76 | 77 | # Logging configuration 78 | [loggers] 79 | keys = root,sqlalchemy,alembic 80 | 81 | [handlers] 82 | keys = console 83 | 84 | [formatters] 85 | keys = generic 86 | 87 | [logger_root] 88 | level = WARN 89 | handlers = console 90 | qualname = 91 | 92 | [logger_sqlalchemy] 93 | level = WARN 94 | handlers = 95 | qualname = sqlalchemy.engine 96 | 97 | [logger_alembic] 98 | level = INFO 99 | handlers = 100 | qualname = alembic 101 | 102 | [handler_console] 103 | class = StreamHandler 104 | args = (sys.stderr,) 105 | level = NOTSET 106 | formatter = generic 107 | 108 | [formatter_generic] 109 | format = %(levelname)-5.5s [%(name)s] %(message)s 110 | datefmt = %H:%M:%S 111 | -------------------------------------------------------------------------------- /fastapi/api.py: -------------------------------------------------------------------------------- 1 | """ from fastapi import APIRouter 2 | import users 3 | 4 | router = APIRouter() 5 | router.include_router(users.users) 6 | """ 7 | -------------------------------------------------------------------------------- /fastapi/auth.py: -------------------------------------------------------------------------------- 1 | from passlib.context import CryptContext 2 | import models_and_schemas 3 | from jose import jwt 4 | from datetime import datetime, timedelta 5 | from fastapi.security import OAuth2PasswordBearer 6 | from fastapi import Depends, HTTPException 7 | from config import settings 8 | 9 | pwd_context = CryptContext(schemes=["bcrypt"]) 10 | oauth2_scheme = OAuth2PasswordBearer(tokenUrl="login") 11 | 12 | 13 | def create_password_hash(password): 14 | return pwd_context.hash(password) 15 | 16 | 17 | def verify_password(plain_password, hashed_password): 18 | return pwd_context.verify(plain_password, hashed_password) 19 | 20 | 21 | def create_access_token(user: models_and_schemas.User): 22 | claims = { 23 | "sub": user.username, 24 | "name": user.full_name, 25 | "role": user.role, 26 | "exp": datetime.utcnow() + timedelta(days=settings.JWT_EXP_DAYS) 27 | } 28 | return jwt.encode(claims=claims, key=settings.SECRET_KEY, algorithm="HS256") 29 | 30 | 31 | def decode_token(token): 32 | claims = jwt.decode(token, key=settings.SECRET_KEY) 33 | return claims 34 | 35 | 36 | def get_username_by_token(token: str = Depends(oauth2_scheme)): 37 | claims = jwt.decode(token, key=settings.SECRET_KEY) 38 | return claims.get('sub') 39 | 40 | 41 | def check_teacher(token: str = Depends(oauth2_scheme)): 42 | claims = decode_token(token) 43 | role = claims.get('role') 44 | if role != "teacher": 45 | raise HTTPException( 46 | status_code=403, 47 | detail="Only teachers!", 48 | headers={"WWW-Authenticate": "Bearer"} 49 | ) 50 | return claims 51 | -------------------------------------------------------------------------------- /fastapi/config.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseSettings 2 | import os 3 | 4 | 5 | class Settings(BaseSettings): 6 | SECRET_KEY: str = os.urandom(24) 7 | TEACHER_KEY: str = os.urandom(10) 8 | GITEA_USERNAME: str = "schoco" 9 | GITEA_PASSWORD: str = "schoco1234" 10 | GITEA_LOCALHOST_PORT: int = 3000 11 | GITEA_HOST: str = "" 12 | MAX_CONTAINERS: int = 8 13 | PRODUCTION: bool = True 14 | JWT_EXP_DAYS: int = 15 15 | FULL_DATA_PATH: str = "/app/data" 16 | BACKEND_VER: str = "1.3.1" 17 | 18 | 19 | settings = Settings() 20 | -------------------------------------------------------------------------------- /fastapi/database_config.py: -------------------------------------------------------------------------------- 1 | from sqlmodel import Session, SQLModel, create_engine 2 | from config import settings 3 | 4 | if settings.PRODUCTION: 5 | SQLITE_DATABASE_URL = "sqlite:////app/data/sql_app.db" 6 | else: 7 | SQLITE_DATABASE_URL = "sqlite:///./data/sql_app.db" 8 | 9 | engine = create_engine(SQLITE_DATABASE_URL, connect_args={ 10 | "check_same_thread": False}) 11 | 12 | 13 | def get_db(): 14 | with Session(engine) as session: 15 | yield session 16 | 17 | 18 | def create_db_and_tables(): 19 | SQLModel.metadata.create_all(engine) 20 | -------------------------------------------------------------------------------- /fastapi/gunicorn_conf.py: -------------------------------------------------------------------------------- 1 | import json 2 | import multiprocessing 3 | import os 4 | 5 | workers_per_core_str = os.getenv("WORKERS_PER_CORE", "1") 6 | max_workers_str = os.getenv("MAX_WORKERS") 7 | use_max_workers = None 8 | if max_workers_str: 9 | use_max_workers = int(max_workers_str) 10 | web_concurrency_str = os.getenv("WEB_CONCURRENCY", None) 11 | 12 | host = os.getenv("HOST", "0.0.0.0") 13 | port = os.getenv("PORT", "80") 14 | bind_env = os.getenv("BIND", None) 15 | use_loglevel = os.getenv("LOG_LEVEL", "debug") 16 | if bind_env: 17 | use_bind = bind_env 18 | else: 19 | use_bind = f"{host}:{port}" 20 | 21 | cores = multiprocessing.cpu_count() 22 | workers_per_core = float(workers_per_core_str) 23 | default_web_concurrency = workers_per_core * cores 24 | if web_concurrency_str: 25 | web_concurrency = int(web_concurrency_str) 26 | assert web_concurrency > 0 27 | else: 28 | web_concurrency = max(int(default_web_concurrency), 2) 29 | if use_max_workers: 30 | web_concurrency = min(web_concurrency, use_max_workers) 31 | accesslog_var = os.getenv("ACCESS_LOG", "-") 32 | use_accesslog = accesslog_var or None 33 | errorlog_var = os.getenv("ERROR_LOG", "-") 34 | use_errorlog = errorlog_var or None 35 | graceful_timeout_str = os.getenv("GRACEFUL_TIMEOUT", "120") 36 | timeout_str = os.getenv("TIMEOUT", "120") 37 | keepalive_str = os.getenv("KEEP_ALIVE", "5") 38 | 39 | # Gunicorn config variables 40 | loglevel = use_loglevel 41 | workers = web_concurrency 42 | bind = use_bind 43 | errorlog = use_errorlog 44 | worker_tmp_dir = "/dev/shm" 45 | accesslog = use_accesslog 46 | graceful_timeout = int(graceful_timeout_str) 47 | timeout = int(timeout_str) 48 | keepalive = int(keepalive_str) 49 | 50 | 51 | # For debugging and testing 52 | log_data = { 53 | "loglevel": loglevel, 54 | "workers": workers, 55 | "bind": bind, 56 | "graceful_timeout": graceful_timeout, 57 | "timeout": timeout, 58 | "keepalive": keepalive, 59 | "errorlog": errorlog, 60 | "accesslog": accesslog, 61 | # Additional, non-gunicorn variables 62 | "workers_per_core": workers_per_core, 63 | "use_max_workers": use_max_workers, 64 | "host": host, 65 | "port": port, 66 | } 67 | print(json.dumps(log_data)) 68 | 69 | preload_app = True 70 | -------------------------------------------------------------------------------- /fastapi/java_helloWorld/de/Schoco.java: -------------------------------------------------------------------------------- 1 | public class { 2 | // Hier kannst du Instanzvariablen deklarieren, z.B.: 3 | // private String name; 4 | 5 | /** 6 | * Konstruktor für die -Klasse 7 | */ 8 | public () { 9 | 10 | } 11 | 12 | #main#public static void main(String[] args) { 13 | object1 = new (); 14 | }!#main# 15 | } 16 | -------------------------------------------------------------------------------- /fastapi/java_helloWorld/de/Tests.java: -------------------------------------------------------------------------------- 1 | import static org.junit.Assert.*; 2 | import org.junit.BeforeClass; 3 | import org.junit.Test; 4 | 5 | // 🚀 Im Wiki gibts eine Anleitung: https://github.com/PhiTux/schoco/wiki/JUnit-Tests-anlegen-%28de%29 6 | 7 | /** 8 | * ⭐⭐⭐ 9 | * Diese Klasse / Datei ist für die Schüler/innen NICHT sichtbar. 10 | * Sie ist die einzige Datei, die JUnit-Tests enthalten darf. 11 | * Sie kann NICHT umbenannt werden. 12 | * ⭐⭐⭐ 13 | */ 14 | public class Tests { 15 | /** 🛑 Ändere nichts an diesem ersten Codeblock!! 16 | * Er aktiviert den Security-Manager, der die Rechte des Schülercodes während der Tests einschränkt. 17 | * Dadurch können Sicherheitslücken verhindert werden. */ 18 | @BeforeClass 19 | public static void setUp() { 20 | System.setSecurityManager(new SecurityManager()); 21 | } 22 | /* 🛑 Ende des Security-Codes 23 | *================================================*/ 24 | 25 | 26 | // Zwei Beispiele für JUnit-Tests 27 | @Test 28 | public void isGreaterTrue() { 29 | /*Schoco schoco = new Schoco(); 30 | assertTrue("Num 1 is greater than Num 2", schoco.isGreater(4, 3)); 31 | assertTrue(schoco.isGreater(4, 3));*/ 32 | } 33 | 34 | @Test 35 | public void isGreaterFalse() { 36 | /*Schoco schoco = new Schoco(); 37 | assertEquals(4, schoco.addition(1, 3));*/ 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /fastapi/java_helloWorld/en/Schoco.java: -------------------------------------------------------------------------------- 1 | public class { 2 | // Here you can declare instance variables like: 3 | // private String name; 4 | 5 | /** 6 | * Constructor for the class 7 | */ 8 | public () { 9 | 10 | } 11 | 12 | #main#public static void main(String[] args) { 13 | object1 = new (); 14 | }!#main# 15 | } 16 | -------------------------------------------------------------------------------- /fastapi/java_helloWorld/en/Tests.java: -------------------------------------------------------------------------------- 1 | import static org.junit.Assert.*; 2 | import org.junit.BeforeClass; 3 | import org.junit.Test; 4 | 5 | // 🚀 See the wiki for explanation: https://github.com/PhiTux/schoco/wiki/Create-JUnit-Tests-%28en%29 6 | 7 | /** 8 | * ⭐⭐⭐ 9 | * This class / file is NOT visible to students. 10 | * It is the only file which may contain the JUnit-Tests. 11 | * You can NOT rename this file. 12 | * ⭐⭐⭐ 13 | */ 14 | public class Tests { 15 | /** 🛑 don't touch the following code-block!! 16 | * It activates the security-manager which ensures limited rights of student's code during Testing. */ 17 | @BeforeClass 18 | public static void setUp() { 19 | System.setSecurityManager(new SecurityManager()); 20 | } 21 | /* 🛑 end of security code 22 | *================================================*/ 23 | 24 | 25 | // Two examples for JUnit-Tests 26 | @Test 27 | public void isGreaterTrue() { 28 | /*Schoco schoco = new Schoco(); 29 | assertTrue("Num 1 is greater than Num 2", schoco.isGreater(4, 3)); 30 | assertTrue(schoco.isGreater(4, 3));*/ 31 | } 32 | 33 | @Test 34 | public void isGreaterFalse() { 35 | /*Schoco schoco = new Schoco(); 36 | assertEquals(4, schoco.addition(1, 3));*/ 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /fastapi/main.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI, Request 2 | from fastapi.middleware.cors import CORSMiddleware 3 | from fastapi_jwt_auth.exceptions import AuthJWTException 4 | from fastapi.responses import JSONResponse 5 | from multiprocessing import Lock, Manager 6 | import users 7 | import code 8 | import cookies_api 9 | from fastapi_utils.tasks import repeat_every 10 | from fastapi.logger import logger 11 | import logging 12 | 13 | gunicorn_logger = logging.getLogger('gunicorn.error') 14 | logger.handlers = gunicorn_logger.handlers 15 | if __name__ != "main": 16 | logger.setLevel(gunicorn_logger.level) 17 | else: 18 | logger.setLevel(logging.DEBUG) 19 | 20 | 21 | app = FastAPI() 22 | 23 | app.include_router(users.users) 24 | app.include_router(code.code) 25 | 26 | 27 | @app.exception_handler(AuthJWTException) 28 | def authjwt_exception_handler(request: Request, exc: AuthJWTException): 29 | return JSONResponse( 30 | status_code=exc.status_code, 31 | content={"detail": exc.message} 32 | ) 33 | 34 | 35 | origins = [ 36 | "http://lab:5173", 37 | "http://127.0.0.1:5173", 38 | "http://localhost:5173", 39 | ] 40 | 41 | app.add_middleware(CORSMiddleware, allow_origins=origins, 42 | allow_credentials=True, allow_methods=["*"], allow_headers=["*"], expose_headers=["content-disposition"]) 43 | 44 | 45 | lock = Lock() 46 | 47 | m = Manager() 48 | firstRun = m.Value(bool, True) 49 | 50 | 51 | # we repeat it every 60 seconds to refill the queue (in case there's some hickup...) 52 | @app.on_event("startup") 53 | @repeat_every(seconds=60) 54 | def startup_event(): 55 | # logger.info("startup_event") 56 | if firstRun.value: 57 | firstRun.value = False 58 | # delete all container_dirs from previous runs 59 | cookies_api.remove_all_container_dirs() 60 | 61 | with lock: 62 | cookies_api.fillNewContainersQueue() 63 | else: 64 | with lock: 65 | cookies_api.refillNewContainersQueue() 66 | 67 | 68 | @app.on_event("shutdown") 69 | def shutdown_event(): 70 | # delete all containers and container_dirs 71 | cookies_api.kill_all_containers() 72 | cookies_api.remove_all_container_dirs() 73 | logger.info("Goodbye!") 74 | -------------------------------------------------------------------------------- /fastapi/models_and_schemas.py: -------------------------------------------------------------------------------- 1 | from sqlmodel import SQLModel, Field, Relationship 2 | from enum import Enum 3 | from typing import Optional, List 4 | from pydantic import BaseModel 5 | 6 | # alembic readme: 7 | """ 8 | When updating the database models, you need to create a new revision with Alembic. 9 | BEFORE creating the model, make sure, that your old DB has included the metadata of the old alembic-revision. 10 | -> call: alembic -c alembic_dev.ini upgrade head 11 | 12 | Then edit the models and create a new revision: 13 | -> call: alembic -c alembic_dev.ini revision --autogenerate -m "EXCPLAIN WHAT YOU CHANGED" 14 | 15 | Afterwards actually update the dev-database: 16 | -> call: alembic -c alembic_dev.ini upgrade head 17 | """ 18 | 19 | # database models 20 | 21 | 22 | class UserCourseLink(SQLModel, table=True): 23 | user_id: Optional[int] = Field( 24 | default=None, foreign_key="user.id", primary_key=True) 25 | course_id: Optional[int] = Field( 26 | default=None, foreign_key="course.id", primary_key=True) 27 | 28 | 29 | class Roles(str, Enum): 30 | pupil = "pupil" 31 | teacher = "teacher" 32 | 33 | 34 | class BaseUser(SQLModel): 35 | username: str = Field(unique=True) 36 | full_name: str 37 | role: Roles 38 | 39 | 40 | class User(BaseUser, table=True): 41 | id: Optional[int] = Field(default=None, primary_key=True) 42 | hashed_password: str 43 | must_change_password: Optional[bool] = False 44 | 45 | courses: List["Course"] = Relationship( 46 | back_populates="users", link_model=UserCourseLink) 47 | 48 | projects: List["Project"] = Relationship(back_populates="owner") 49 | 50 | homeworks: List["EditingHomework"] = Relationship(back_populates="owner") 51 | 52 | 53 | class UserSchema(BaseUser): 54 | password: str 55 | 56 | 57 | class Course(SQLModel, table=True): 58 | id: Optional[int] = Field(default=None, primary_key=True) 59 | name: str = Field(unique=True) 60 | color: str 61 | fontDark: bool 62 | 63 | users: List["User"] = Relationship( 64 | back_populates="courses", link_model=UserCourseLink) 65 | homeworks: List["Homework"] = Relationship(back_populates="course") 66 | 67 | 68 | class Project(SQLModel, table=True): 69 | id: Optional[int] = Field(default=None, primary_key=True) 70 | uuid: str = Field(unique=True) 71 | name: str 72 | description: str 73 | owner_id: int = Field(foreign_key="user.id") 74 | computation_time: Optional[int] = 10 75 | main_class: str 76 | 77 | owner: "User" = Relationship(back_populates="projects") 78 | 79 | 80 | class Homework(SQLModel, table=True): 81 | id: Optional[int] = Field(default=None, primary_key=True) 82 | course_id: int = Field( 83 | default=None, foreign_key="course.id") 84 | template_project_id: int = Field( 85 | default=None, foreign_key="project.id", unique=True) 86 | original_project_id: int = Field( 87 | default=None, foreign_key="project.id") 88 | deadline: str # datetime 89 | solution_project_id: Optional[int] = Field( 90 | default=None, foreign_key="project.id") 91 | # datetime, when the solution may be shown to pupils 92 | solution_start_showing: Optional[str] = "" 93 | enable_tests: Optional[bool] = True 94 | 95 | course: "Course" = Relationship(back_populates="homeworks") 96 | 97 | 98 | class EditingHomework(SQLModel, table=True): 99 | id: Optional[int] = Field(default=None, primary_key=True) 100 | # uuid: str = Field(unique=True) -> get uuid from howework->template-project 101 | homework_id: int = Field( 102 | default=None, foreign_key="homework.id") 103 | owner_id: int = Field( 104 | default=None, foreign_key="user.id") # owner_id = branch_name 105 | 106 | submission: Optional[str] = "" 107 | # submission having the structure {passed_tests: ..., failed_tests: ...} 108 | 109 | number_of_compilations: Optional[int] = 0 110 | number_of_runs: Optional[int] = 0 111 | number_of_tests: Optional[int] = 0 112 | 113 | owner: "User" = Relationship() 114 | homework: "Homework" = Relationship() 115 | 116 | # other models 117 | 118 | 119 | class newPupil(BaseModel): 120 | fullname: str 121 | username: str 122 | password: str 123 | 124 | 125 | class pupilsList(BaseModel): 126 | newPupils: List[newPupil] 127 | courseIDs: List[int] 128 | 129 | 130 | class setPassword(BaseModel): 131 | username: str 132 | password: str 133 | 134 | 135 | class AddUserCourseLink(BaseModel): 136 | user_id: int 137 | coursename: str 138 | 139 | 140 | class UserById(BaseModel): 141 | user_id: int 142 | 143 | 144 | class changeName(BaseModel): 145 | user_id: int 146 | name: str 147 | 148 | 149 | class newProject(BaseModel): 150 | projectName: str 151 | className: str 152 | projectDescription: str 153 | language: str 154 | 155 | 156 | class updateText(BaseModel): 157 | text: str 158 | 159 | 160 | class FileChanges(BaseModel): 161 | path: str 162 | content: str 163 | sha: str 164 | 165 | 166 | class FileChangesList(BaseModel): 167 | changes: List[FileChanges] 168 | 169 | 170 | class File(BaseModel): 171 | path: str 172 | content: str 173 | 174 | 175 | class filesList(BaseModel): 176 | files: List[File] 177 | 178 | 179 | class startCompile(BaseModel): 180 | port: int 181 | container_uuid: str 182 | save_output: bool 183 | 184 | 185 | class startExecute(BaseModel): 186 | port: int 187 | container_uuid: str 188 | save_output: bool 189 | 190 | 191 | class startTest(BaseModel): 192 | port: int 193 | container_uuid: str 194 | 195 | 196 | class create_homework(BaseModel): 197 | files: List[File] 198 | course_id: int 199 | deadline_date: str 200 | computation_time: int 201 | enable_tests: bool 202 | 203 | 204 | class homeworkId(BaseModel): 205 | id: int 206 | 207 | 208 | class courseID(BaseModel): 209 | id: int 210 | 211 | 212 | class RenameFile(BaseModel): 213 | old_path: str 214 | new_path: str 215 | content: str 216 | sha: str 217 | 218 | 219 | class RenameHomework(BaseModel): 220 | id: int 221 | new_name: str 222 | 223 | 224 | class ChangePassword(BaseModel): 225 | oldPassword: str 226 | newPassword: str 227 | 228 | 229 | class AddClass(BaseModel): 230 | className: str 231 | language: str 232 | 233 | 234 | class DeleteFile(BaseModel): 235 | path: str 236 | sha: str 237 | 238 | 239 | class UpdateHomeworkSettings(BaseModel): 240 | id: int 241 | deadline_date: str 242 | computation_time: int 243 | enable_tests: bool 244 | 245 | 246 | class UUID(BaseModel): 247 | uuid: str 248 | 249 | 250 | class Password(BaseModel): 251 | password: str 252 | 253 | 254 | class AddSolution(BaseModel): 255 | homework_id: int 256 | solution_id: int 257 | solution_start_showing: str 258 | 259 | 260 | class DeleteSolution(BaseModel): 261 | homework_id: int 262 | 263 | 264 | class EntryPoint(BaseModel): 265 | entry_point: str 266 | 267 | 268 | class SkipVersion(BaseModel): 269 | skip_version: str 270 | 271 | 272 | class ComputationTime(BaseModel): 273 | computation_time: int 274 | -------------------------------------------------------------------------------- /fastapi/prestart.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | mkdir -p ./data/containers 4 | mkdir -p ./data/projects 5 | 6 | # call alembic upgrades 7 | alembic -c alembic_prod.ini upgrade head 8 | -------------------------------------------------------------------------------- /fastapi/requirements.txt: -------------------------------------------------------------------------------- 1 | fastapi==0.86.0 2 | fastapi_jwt_auth==0.5.0 3 | jose==1.0.0 4 | passlib==1.7.4 5 | python_jose==3.3.0 6 | sqlmodel==0.0.8 7 | uvicorn 8 | python-multipart 9 | bcrypt==4.0.1 10 | pycurl==7.45.2 11 | docker 12 | fastapi_utils 13 | alembic 14 | -------------------------------------------------------------------------------- /frontend/.env.development: -------------------------------------------------------------------------------- 1 | VITE_API_URL=http://lab:8000/api/ 2 | #VITE_API_URL=http://0.0.0.0:8000/api/ 3 | VITE_APP_TITLE=Schoco -------------------------------------------------------------------------------- /frontend/.env.production: -------------------------------------------------------------------------------- 1 | VITE_API_URL=/api/ 2 | VITE_APP_TITLE=Schoco -------------------------------------------------------------------------------- /frontend/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /frontend/README.md: -------------------------------------------------------------------------------- 1 | # Vue 3 + Vite 2 | 3 | This template should help get you started developing with Vue 3 in Vite. The template uses Vue 3 ` 12 | 13 | 14 | -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "schoco", 3 | "private": true, 4 | "version": "1.3.1", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vite build", 9 | "preview": "vite preview" 10 | }, 11 | "dependencies": { 12 | "@fortawesome/fontawesome-svg-core": "^6.5.1", 13 | "@fortawesome/free-solid-svg-icons": "^6.5.1", 14 | "@fortawesome/vue-fontawesome": "^3.0.5", 15 | "@popperjs/core": "^2.11.8", 16 | "@vuepic/vue-datepicker": "^4.5.1", 17 | "ace-builds": "^1.32.3", 18 | "axios": "^1.6.4", 19 | "bootstrap": "^5.3.2", 20 | "jwt-decode": "^3.1.2", 21 | "lodash": "^4.17.21", 22 | "pinia": "^2.1.7", 23 | "splitpanes": "^3.1.5", 24 | "vue-router": "^4.2.5", 25 | "vue-select": "^4.0.0-beta.6", 26 | "vue-upload-component": "^3.1.8", 27 | "vue3-ace-editor": "^2.2.4", 28 | "vue3-text-clamp": "^0.1.2" 29 | }, 30 | "devDependencies": { 31 | "@vitejs/plugin-vue": "^4.6.2", 32 | "sass": "^1.69.7", 33 | "vite": "^4.5.1", 34 | "vue-i18n": "^9.8.0", 35 | "vue": "^3.2.45" 36 | } 37 | } -------------------------------------------------------------------------------- /frontend/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PhiTux/schoco/c302238b5c42b8125eff5d3bfd2d4a2485f14f69/frontend/public/favicon.ico -------------------------------------------------------------------------------- /frontend/public/schoco-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 110 | -------------------------------------------------------------------------------- /frontend/public/schoco-text-1.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 14 | 24 | 26 | 31 | 35 | 39 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /frontend/public/schoco-text-2.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 14 | 24 | 26 | 31 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /frontend/public/schoco-text-3.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 14 | 24 | 26 | 31 | 35 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /frontend/src/App.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 38 | 39 | 118 | 119 | 128 | -------------------------------------------------------------------------------- /frontend/src/components/ColorModeSwitch.vue: -------------------------------------------------------------------------------- 1 | 58 | 59 | 85 | 86 | -------------------------------------------------------------------------------- /frontend/src/components/CourseBadge.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 40 | 41 | 61 | -------------------------------------------------------------------------------- /frontend/src/components/IDEFileTree.vue: -------------------------------------------------------------------------------- 1 | 61 | 62 | 82 | 83 | 88 | -------------------------------------------------------------------------------- /frontend/src/components/PasswordInfo.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | -------------------------------------------------------------------------------- /frontend/src/components/PasswordInput.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 45 | 46 | -------------------------------------------------------------------------------- /frontend/src/components/Submission.vue: -------------------------------------------------------------------------------- 1 | 27 | 28 | 33 | 34 | -------------------------------------------------------------------------------- /frontend/src/components/TreeNode.vue: -------------------------------------------------------------------------------- 1 | 60 | 61 | 109 | 110 | -------------------------------------------------------------------------------- /frontend/src/fonts/NotoSans-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PhiTux/schoco/c302238b5c42b8125eff5d3bfd2d4a2485f14f69/frontend/src/fonts/NotoSans-Regular.ttf -------------------------------------------------------------------------------- /frontend/src/locales/de/color_mode_switch.js: -------------------------------------------------------------------------------- 1 | export const color_mode_switch_de = { 2 | dark: "Dark", 3 | light: "Light", 4 | auto: "Auto", 5 | } -------------------------------------------------------------------------------- /frontend/src/locales/de/general.js: -------------------------------------------------------------------------------- 1 | export const general_de = { 2 | locale: "de", 3 | long_date_format: "E dd.MM.yyyy, HH:mm", // E is abbreviated weekday, e.g. "Mon" 4 | name: "Name", 5 | username: "Username", 6 | password: "Passwort", 7 | current_password: "Aktuelles Passwort", 8 | new_password: "Neues Passwort", 9 | confirm_password: "Passwort bestätigen", 10 | full_name: "Vor- und Nachname", 11 | input_incomplete: "Eingabe unvollständig", 12 | whitespace_in_username_not_allowed: "Benutzernamen dürfen keine Leerzeichen enthalten!", 13 | passwords_not_identical: "Die Passwörter stimmen nicht überein", 14 | password_at_least_8: "Passwort muss mindestens 8 Zeichen enthalten!", 15 | password_doesnt_fullfill_requirements: "Passwort erfüllt nicht die Anforderungen!", 16 | password_invalid: "Passwort ist ungültig!", 17 | save_new_password: "Neues Passwort speichern", 18 | abort: "Abbrechen", 19 | close: "Schließen", 20 | rename: "Umbenennen", 21 | duplicate: "Duplizieren", 22 | got_it: "Verstanden", 23 | save: "Speichern", 24 | save_changes: "Änderungen speichern", 25 | question_save_changes: "Änderungen speichern?", 26 | dont_save: "Nicht speichern", 27 | delete: "Löschen", 28 | download: "Herunterladen", 29 | download_project: "Projekt herunterladen", 30 | toastDownloadProjectError: "Fehler beim Download des Projekts.", 31 | or: "oder", 32 | more: "mehr", 33 | less: "weniger", 34 | select_date: "Datum wählen...", 35 | input_missing: "Eingabe fehlt", 36 | project_name: "Projektname", 37 | class_name: "Java-Klassenname", 38 | project_description: "Projektbeschreibung", 39 | upload: "Hochladen", 40 | change_password: "Passwort ändern", 41 | back: "Zurück", 42 | continue: "Weiter", 43 | course: "Kurs", 44 | courses: "Kurse", 45 | database_id: "#id", 46 | role: "Rolle", 47 | teacher: "Lehrer/in", 48 | pupil: "Schüler/in", 49 | settings: "Einstellungen", 50 | edit_settings: "Einstellungen bearbeiten", 51 | deadline: "Abgabefrist", 52 | utc: "UTC", 53 | editing_time: "Bearbeitungszeit", 54 | days: "Tage", 55 | hours: "Stunden", 56 | seconds: "Sekunden", 57 | computation_time: "Rechenzeit", 58 | computation_time_description: "Lege fest, wie viele Sekunden Rechenzeit (bzw. genauer: Laufzeit) auf dem Server pro Aktion zur Verfügung stehen. Als Aktion gilt:
  • Kompilieren
  • Ausführen
  • Testen
Der Standardwert beträgt 5 Sekunden, welchen Schüler/innen in eigenen Projekten auch nicht verändern können, da der Server mit endlos laufenden Programmen lahm gelegt werden könnte. Unter Umständen kann es aber sinnvoll sein, bei Aufgaben die Laufzeit zu verlängern, z. B. wenn ein Programm auf Benutzereingaben warten muss, welche auch ihre Zeit brauchen.", 59 | at_least_3_default_5: "Mindestens 3, Standard 5", 60 | result: "Ergebnis", 61 | open: "Öffnen", 62 | error_changing_password: "Fehler beim Ändern des Passworts.", 63 | success_changing_password: "Passwort erfolgreich geändert.", 64 | toastRenameProjectSuccess: "Projekt wurde umbenannt.", 65 | toastRenameProjectError: "Fehler beim Umbenennen des Projekts.", 66 | add: "Hinzufügen", 67 | restart: "Neu beginnen", 68 | restart_assignment: "Aufgabe neu beginnen", 69 | template: "Vorlage", 70 | input: "Eingabe", 71 | send: "Senden", 72 | project: "Projekt", 73 | usermanagement: "Benutzerverwaltung", 74 | version: "Version", 75 | enable_tests: "Tests aktivieren", 76 | enable_tests_question: "Tests aktivieren?", 77 | enable_tests_description: "Sollen den Schülerinnen bei dieser Aufgabe Tests zur Verfügung stehen? Dazu muss die Datei Tests.java sinnvolle JUnit-Tests enthalten. Andernfalls sehen die Schüler nicht den Testen-Knopf und die Lehrkraft sieht natürlich auch nicht mit einem Blick eine Übersicht der Ergebnisse.", 78 | use_test_functions: "Test-Funktionen nutzen", 79 | activate_tests: "Tests aktivieren", 80 | deactivate_tests: "Tests deaktivieren", 81 | } -------------------------------------------------------------------------------- /frontend/src/locales/de/home.js: -------------------------------------------------------------------------------- 1 | export const home_de = { 2 | home_title: "Home", 3 | new_project: "Neues Projekt", 4 | toastStartHomeworkError: "Fehler beim Starten der Aufgabe. Probiere es erneut und frage andernfalls deine Lehrkraft um Hilfe.", 5 | toastAddSolutionSuccess: "Lösung wurde hinzugefügt.", 6 | toastAddSolutionError: "Fehler beim Zufügen der Lösung.", 7 | toastDuplicateProjectSuccess: "Projekt wurde dupliziert.", 8 | toastDuplicateProjectError: "Fehler beim Duplizieren des Projekts.", 9 | toastRenameHomeworkSuccess: "Aufgabe wurde umbenannt.", 10 | toastRenameHomeworkError: "Fehler beim Umbenennen der Aufgabe.", 11 | toastDeleteProjectError: "Fehler beim Löschen des Projekts / der Aufgabe.", 12 | toastDeleteProjectSuccess: "Das Projekt oder die Aufgabe wurde gelöscht.", 13 | rename_assignment: "Aufgabe umbenennen", 14 | enter_new_assignment_name: 'Gib den neuen Namen der Aufgabe {0} ein:', 15 | rename_project: "Projekt umbenennen", 16 | enter_new_project_name: 'Gib den neuen Namen des Projekts {0} ein:', 17 | delete_private_project: "Privates Projekt löschen?", 18 | want_to_delete_private_project: "Möchtest du wirklich dein eigenes, privates Projekt mit dem Titel {0} löschen?", 19 | delete_private_project_teacher_warning: "Falls dieses Projekt als Lösung hinterlegt ist, dann wird auch diese Verknüpfung gelöscht.", 20 | delete_progress: "Fortschritt löschen?", 21 | want_to_delete_progress: "Du kannst eine Aufgabe nicht völlig löschen. Du kannst nur deinen bisherigen Fortschritt löschen und anschließend wieder in einem \"sauberen\" Projekt von vorne beginnen. {0}{1} Möchtest du wirklich deinen Fortschritt von {2} löschen?", 22 | delete_assignment: "Aufgabe löschen?", 23 | want_to_delete_assignment: "Wenn du eine Aufgabe löschst, dann wird sie auch bei allen Schüler/innen gelöscht. Möchtest du wirklich die Aufgabe mit dem Titel {0} löschen?", 24 | add_solution: "Lösung hinzufügen", 25 | change_solution: "Lösung ändern", 26 | add_solution_info: "Wähle ein privates Projekt aus, welches die Schüler ab einem bestimmten Zeitpunkt als Lösung anschauen können. Die Schüler können die Lösung nur öffnen und ausführen, aber nicht bearbeiten.", 27 | select_the_solution: "Wähle die Lösung:", 28 | solution_visible_from_when: "Ab wann soll die Lösung sichtbar sein?", 29 | from_right_now: "Ab sofort", 30 | visible_from_now: 'Sichtbar ab sofort', 31 | visible_in_x_days_hours: "Sichtbar in {0}, {1}", 32 | visible_in_x_days: '{n} Tagen | {n} Tag | {n} Tagen', // 0, 1 or more days 33 | visible_in_x_hours: '{n} Stunden | {n} Stunde | {n} Stunden', // 0, 1 or more hours 34 | add_solution_input_invalid: "Lösung konnte nicht gespeichert werden. Überprüfe deine Eingaben.", 35 | project_search: "Projektsuche", 36 | current_assignment: "Aktuelle Aufgaben", 37 | previous_assignments: "Frühere Aufgaben", 38 | my_projects: "Meine Projekte", 39 | delete_solution: "Lösung löschen", 40 | delete_solution_confirm: "Möchtest du die Verlinkung zur Lösung wirklich löschen? Dein privates Projekt bleibt davon unberührt.", 41 | toastDeleteSolutionError: "Fehler beim Löschen der Lösung.", 42 | toastDeleteSolutionSuccess: "Die Lösung wurde gelöscht.", 43 | filter_courses: "Kurse filtern", 44 | } -------------------------------------------------------------------------------- /frontend/src/locales/de/homework.js: -------------------------------------------------------------------------------- 1 | export const homework_de = { 2 | unknown_assignment: "Unbekannte Aufgabe", 3 | error_on_changing_settings: "Fehler beim Ändern der Einstellungen.", 4 | settings_updated: "Einstellungen aktualisiert.", 5 | cant_change_course_of_assignment: "Du kannst bei einer bereits erstellten Aufgabe den Kurs nicht mehr ändern!
Du kannst die Aufgabe nur in Home löschen und anschließend für einen anderen Kurs neu erstellen.", 6 | already_expired: "Bereits abgelaufen", 7 | show_results: "Zeige Ergebnisse", 8 | edit_template: "Vorlage bearbeiten", 9 | number_of_compilations: "# Kompilierungen", 10 | number_of_runs: "# Ausführungen", 11 | number_of_tests: "# Tests", 12 | } -------------------------------------------------------------------------------- /frontend/src/locales/de/ide.js: -------------------------------------------------------------------------------- 1 | export const ide_de = { 2 | server_was_overloaded_try_again: "Der Server war vermutlich gerade überlastet 😥 Bitte versuche es erneut!", 3 | no_live_output_result_to_follow: "Live-Ausgabe nicht möglich, Ergebnis folgt gleich...", 4 | compilation_successful: "Erfolgreich kompiliert 🎉", 5 | compilation_started: "Kompilierung gestartet... 🛠", 6 | compilation_error: "Interner Server-Fehler beim Kompilieren! ❌", 7 | program_running: "Programm wird ausgeführt...", 8 | program_testing: "Programm wird getestet 📝➡️✅ bitte warten...", 9 | no_executables_found: "🔎 Leider keine ausführbaren Dateien gefunden. Bitte zuerst kompilieren ⚙", 10 | program_exited_without_output: "Programm wurde (erfolgreich, aber ohne Ausgabe) beendet! ✔", 11 | program_exited_probably_with_error: "Programm wurde (vermutlich fehlerhaft) beendet! ❌", 12 | security_error: "💥🙈 es gab wohl einen Sicherheitsfehler beim Testen deines Programms. Scheinbar hat dein Programm versucht, Dinge auszuführen, die nicht erlaubt sind. Korrigiere dies zuerst.\nWenn das Problem bestehen bleibt, solltest du dich an deine Lehrerin / deinen Lehrer wenden.", 13 | computation_time_exceeded: "❌ Programm wurde frühzeitig beendet! Die Rechenzeit ist vermutlich abgelaufen. Hast du irgendwo eine Endlosschleife?", 14 | all_tests_passed: "Alle Tests bestanden 🎉🤩\n\nDu kannst nun höchstens noch versuchen, deinen Quellcode zu \"verschönern\" ;-)", 15 | no_test_passed: "Ups 🧐 Scheinbar wurde kein einziger Test bestanden! Alle oben gezeigten Tests schlugen fehl.", 16 | error_during_testing: "💥🙈 es gab wohl einen Fehler beim Testen deines Programms. Probiere es erneut!\nStelle zunächst sicher, dass dein Programm ausgeführt werden kann.\nWenn das Problem bestehen bleibt, solltest du dich an deine Lehrerin / deinen Lehrer wenden.", 17 | test_aborted: "❌ Der Test wurde durch den User abgebrochen.", 18 | x_percent_of_tests_passed: "Du hast {percent}% der Tests bestanden. Die obere Ausgabe zeigt alle Tests, die noch NICHT bestanden wurden.", 19 | stopped_by_user: "\nProgramm wurde vom User beendet! ✔", 20 | error_loading_project: "Fehler beim Laden des Projekts. Bitte zurück oder neu laden.", 21 | error_on_saving: "Fehler beim Speichern!", 22 | couldnt_rename_file: "Datei konnte nicht umbenannt werden.", 23 | file_rename_successful: "Datei erfolgreich umbenannt.", 24 | error_saving_project_description: "Fehler beim Speichern der Projektbeschreibung.", 25 | error_project_name_empty: "Projektname darf nicht leer sein!", 26 | project_not_existing_or_no_access: "Projekt existiert nicht, oder du hast keinen Zugriff darauf!", 27 | error_creating_homework: "Fehler beim Erstellen der Aufgabe!", 28 | homework_created_successfully: "Aufgabe erfolgreich erstellt.", 29 | file_created_successfully: "Datei erfolgreich erstellt.", 30 | error_creating_file: "Fehler beim Erstellen der Datei.", 31 | error_deleting_file: "Fehler beim Löschen der Datei.", 32 | error_reseting_homework_progress: "Fehler beim Zurücksetzen der Aufgabe!", 33 | error_stopping_program: "Programm konnte nicht beendet werden!", 34 | changing_template_may_lead_to_inconsistencies: "Änderung der Vorlage kann zu Inkonsistenzen führen!", 35 | description_of_inconsistencies_when_changing_template: "Du bearbeitest gerade die Vorlage einer Aufgabe! Wenn du Änderungen am Code durchführst und speicherst, dann kann dies zu Inkonsistenzen bei den verschiedenen SuS führen:
  • Bei SuS, welche diese Aufgabe bereits bearbeiten, werden deine Änderungen nicht ankommen, solange sie ihren Fortschritt nicht löschen und die HA anschließend neu starten.
  • SuS, welche diese Aufgabe noch nicht gestartet haben, werden beim erstmaligen Öffnen immer den zu diesem Zeitpunkt aktuellsten Code dieser Vorlage verwenden.
Änderungen des Titels oder der Projektbeschreibung sind hingegen kein Problem, können jederzeit geändert werden und werden beim Neuladen der IDE bei den SuS aktualisiert.", 36 | save_changes_before_closing: "Möchtest du die Änderungen vor dem Schließen speichern?", 37 | add_new_class: "Neue Klasse hinzufügen", 38 | description_new_class: "Gib den neuen Namen für die Klasse ein. Denke daran, dass Klassennamen immer mit einem Großbuchstaben beginnen müssen und anschließend nur aus Buchstaben und Zahlen bestehen dürfen (von wenigen Ausnahmen abgesehen)!", 39 | classname_is_invalid: "Klassenname ist ungültig oder existiert bereits!", 40 | question_delete_file: "Klasse löschen?", 41 | ask_delete_class_x: "Möchtest du die Klasse {0} wirklich löschen?", 42 | delete_folder: "Ordner löschen", 43 | delete_folder_description: "Ordner können leider nicht direkt gelöscht werden. Du kannst allerdings sämtlichen Inhalt des Ordners löschen, dann wird der Ordner automatisch mitgelöscht.", 44 | rename_folder: "Ordner umbenennen", 45 | rename_folder_description: "Ordner können leider nicht direkt umbenannt werden. Du kannst allerdings sämtlichen Inhalt des Ordners umbenennen, dann entsteht automatisch ein neuer Ordner mit neuem Namen und der jetzige Ordner wird automatisch gelöscht.

Beispiel:
Du hast einen Ordner old, in dem sich die beiden Dateien First.java und Second.java befinden. Mit vollem Namen lauten diese Dateien old/First.java und old/Second.java. Wenn du die Dateien nacheinander in new/First.java und new/Second.java umbenennst, entspricht das dem Umbenennen des Ordners von old zu new.", 46 | rename_file: "Klasse umbenennen", 47 | rename_file_description: "Gib den neuen Klassennamen für die Klasse {0} ein.", 48 | rename_file_description_2: "Denke daran, anschließend den Inhalt der Klasse an den neuen Namen anzupassen!", 49 | rename_file_invalid: "Dateiname ist ungültig (evtl. enthält er Leerzeichen oder Sonderzeichen oder der Name existiert bereits)!", 50 | create_assignment: "Aufgabe erstellen", 51 | create_assignment_description_1: "⚠️Wichtig: Die Konfiguration eines Projektes sollte vollständig abgeschlossen sein, bevor du daraus eine Aufgabe erstellst. Nach diesem Schritt sollten Änderungen vermieden werden, da die Schüler/innen andernfalls u. U. unterschiedliche Versionen bearbeiten.", 52 | create_assignment_description_2: "Beim Erstellen einer Aufgabe wird der jetzige Zustand deines Projekts kopiert und im Hintergrund \"unsichtbar\" als Vorlage für die Schüler/innen gespeichert. Nach dem Erstellen werden weitere Änderungen an diesem privaten Projekt von dir also nicht in der Aufgabe bei den Schüler/innen sichtbar. Du könntest dieses private Projekt also z. B. anschließend auch gefahrlos löschen und die Aufgabe würde weiterhin bestehen bleiben. Wichtig: Die Schüler/innen können den Einstiegspunkt (= Klasse mit main-Methode) weder ändern noch umbenennen!", 53 | assignment_already_created: "Aus diesem Projekt wurde bereits eine Aufgabe erstellt für folgende Kurse:", 54 | choose_course: "Kurs wählen", 55 | create_course_in_usermanagement: "Du musst zuerst in der Benutzerverwaltung einen Kurs anlegen (idealerweise mit Schülern)!", 56 | create_assignment: "Aufgabe erstellen", 57 | question_restart_assignment: "Aufgabe neu beginnen?", 58 | restart_assignment_description: "Möchtest du deinen bisherigen Fortschritt löschen und anschließend wieder in einem \"sauberen\" Projekt von vorne beginnen?", 59 | new_class: "Neue Klasse", 60 | restart_assignment: "Aufgabe neu beginnen", 61 | keyboard_shortcuts: "Tastaturkürzel", 62 | ctrl: "Strg", 63 | save_changes: "Änderungen speichern", 64 | compile_program: "Programm kompilieren", 65 | execute_program: "Programm ausführen", 66 | test_program: "Programm testen", 67 | compile: "Kompilieren", 68 | execute: "Ausführen", 69 | test: "Testen", 70 | placeholder_for_input: "Eingabe (Entertaste zum Senden)", 71 | set_as_entry_point: "Als Einstiegspunkt setzen", 72 | error_main_class_not_found: "In der Haupt-/Einstiegsklasse (mit 🏠) wurde keine main-Methode gefunden. Stelle sicher, dass dort eine Methode mit folgender Signatur existiert:\npublic static void main(String[] args)", 73 | tooltip_entry_point: "Die Haupt-/Einstiegsklasse des Programms, welche auch die main-Methode enthalten muss.", 74 | edit_computation_time: "Rechenzeit bearbeiten", 75 | success_setting_computation_time: "Rechenzeit erfolgreich gesetzt.", 76 | error_setting_computation_time: "Fehler beim Setzen der Rechenzeit.", 77 | } -------------------------------------------------------------------------------- /frontend/src/locales/de/login.js: -------------------------------------------------------------------------------- 1 | export const login_de = { 2 | login: "Login", 3 | register: "Registrieren", 4 | show_login_error: "Falscher Benutzername oder Passwort", 5 | registration_only_for_teachers: "Registrierung nur für Lehrkräfte", 6 | teacher_key: "Lehrer-Passwort", 7 | input_incomplete: "Eingabe unvollständig.", 8 | registration_successful: "Account erfolgreich erstellt. Bitte einloggen.", 9 | error_msg: "Fehlermeldung", 10 | registration_error: "Account konnte nicht erstellt werden. Möglicherweise stimmt das Lehrer-Passwort nicht, der Benutzer(-name) existiert bereits oder der Benutzername enthält Leerzeichen.", 11 | token_expired: "Deine Sitzung ist abgelaufen. Du musst dich erneut anmelden.", 12 | need_relogin_after_username_change: "Du musst dich nach dem Ändern des Usernamens erneut anmelden.", 13 | } -------------------------------------------------------------------------------- /frontend/src/locales/de/navbar.js: -------------------------------------------------------------------------------- 1 | export const navbar_de = { 2 | password_change_invalid: "Eingabe ungültig!
Stelle sicher, dass dein altes Passwort korrekt ist und dass dein neues Passwort die oberen Kriterien erfüllt.", 3 | logout: "Logout", 4 | update_available: "Update verfügbar", 5 | update_available_text: "Es ist eine neue Version von Schoco verfügbar", 6 | current_version: "Aktuelle Version", 7 | latest_version: "Neueste Version", 8 | new_version: "Neue Version", 9 | current_version: "Aktuelle Version", 10 | read_the_changelog: "Bitte lies dir vor dem Updaten das {0} durch!", 11 | changelog: "Changelog", 12 | skip_version_warning: "Du kannst die Update-Information für die neue Version {0} deaktivieren. Der Update-Hinweis wird dann erst wieder bei der nächsthöheren Version angezeigt.", 13 | skip_version_x: "Version {0} überspringen", 14 | error_skipping_version: "Fehler beim Überspringen/Verstecken der Version {0}.", 15 | success_skipping_version: "Version {0} wird nicht mehr angezeigt.", 16 | } -------------------------------------------------------------------------------- /frontend/src/locales/de/new_project.js: -------------------------------------------------------------------------------- 1 | export const new_project_de = { 2 | new_project_title: "Neues Projekt", 3 | project_name_must_not_be_empty: "Projektname darf nicht leer sein.", 4 | class_name_must_not_be_empty: "Klassenname darf nicht leer sein.", 5 | class_name_illegal: "Klassenname muss mit einem Buchstaben beginnen und darf kein Leerzeichen oder Sonderzeichen enthalten.", 6 | project_could_not_be_created: "Projekt konnte nicht erstellt werden.", 7 | only_zip_files_allowed: "Nur ZIP-Dateien sind erlaubt.", 8 | file_too_large: "Eine ausgewählte Datei ist zu groß (> 5 MB).", 9 | project_import_failed: "Nicht alle Projekte konnten importiert werden!", 10 | project_import_success: "Alle Projekte wurden importiert! Du findest sie nun in {0}.", 11 | new_empty_project: "1) Neues leeres Projekt", 12 | upload_zips: "2) ZIPs hochladen", 13 | create_project: "Projekt erstellen", 14 | upload_zips_description: "Lade zuvor exportierte Projekte hoch. Es können mehrere Projekte auf einmal hochgeladen werden.", 15 | drop_files_to_upload: "Dateien zum Hochladen ablegen", 16 | selected_files: "Ausgewählte Dateien:", 17 | reset_file_list: "Datei-Liste zurücksetzen", 18 | classname_explanation: "Gib den Namen der ersten Java-Klasse an. Sinnvollerweise orieniert sich der Name am Zweck des Projekts. Der Name muss mit einem Großbuchstaben beginnen und darf außer Zahlen keine Sonderzeichen enthalten (von wenigen Ausnahmen abgesehen)." 19 | } -------------------------------------------------------------------------------- /frontend/src/locales/de/password_info.js: -------------------------------------------------------------------------------- 1 | export const password_info_de = { 2 | password_info: "Stelle sicher, dass das neue Passwort mindestens 8 Zeichen lang ist und mindestens zwei der drei folgenden Kriterien erfüllt:
  • Enthält einen Buchstaben
  • Enthält eine Zahl
  • Enthält ein Sonderzeichen
", 3 | } -------------------------------------------------------------------------------- /frontend/src/locales/de/project_card.js: -------------------------------------------------------------------------------- 1 | export const project_card_de = { 2 | submit_until: "Abgabe bis", 3 | begin_assignment: "Aufgabe beginnen", 4 | edit_assignment: "Aufgabe bearbeiten", 5 | show_details: "Details zeigen", 6 | open_project: "Projekt öffnen", 7 | add_solution: "Lösung hinzufügen", 8 | open_solution: "Lösung öffnen", 9 | change_solution_line_break: "Lösung
ändern", 10 | percent_solved: "Zu {0} % gelöst.", 11 | pupils_editing: "Von {0} SuS bearbeitet.", 12 | } -------------------------------------------------------------------------------- /frontend/src/locales/de/users.js: -------------------------------------------------------------------------------- 1 | export const users_de = { 2 | error_confirming_password: "Fehler beim Überprüfen des Passworts!", 3 | name_must_not_be_empty: "Name darf nicht leer sein!", 4 | password_changed_success: "Passwort wurde geändert.", 5 | password_changed_error: "Fehler beim Ändern des Passworts.", 6 | error_on_changing_username: "Fehler beim Ändern des Usernamens. Vielleicht existiert er bereits.", 7 | couldnt_delete_user: "Benutzer {0} {1} konnte nicht gelöscht werden.", 8 | delete_user_success: "Benutzer {0} {1} wurde erfolgreich gelöscht.", 9 | course_created_success: "Kurs wurde erstellt.", 10 | course_created_error: "Fehler beim Erstellen des Kurses. Vielleicht existiert der Kursname bereits?", 11 | course_edited_success: "Kurs wurde bearbeitet.", 12 | course_edited_error: "Fehler beim Bearbeiten des Kurses.", 13 | course_deleted_success: "Kurs wurde gelöscht.", 14 | course_deleted_error: "Fehler beim Löschen des Kurses.", 15 | add_user_to_course_error: "Konnte User nicht zum Kurs zufügen. Ist er bereits Mitglied?", 16 | all_accounts_created: "Es wurden alle Accounts erstellt.", 17 | x_accounts_created: "Es wurden {0} Accounts erstellt.", 18 | following_accounts_not_created: "Folgende Accounts konnten nicht erstellt werden. Vielleicht existieren sie bereits?", 19 | following_accounts_not_created_password_info: "Eventuell gab es Probleme mit den Passwörtern:", 20 | add_users_to_courses_error: "Es gab Fehler bei der Zuordnung von Schülern zu Kursen. Bitte überprüfe die Zuordnung manuell.", 21 | delete_course: "Kurs löschen", 22 | delete_course_confirm: "Möchtest du den Kurs {0} wirklich löschen?", 23 | delete_course_confirm_info: "Damit werden auch Aufgaben gelöscht, die diesem Kurs zugeordnet sind. Außerdem werden alle Schüler aus dem Kurs entfernt.", 24 | create_new_course: "Neuen Kurs erstellen", 25 | course_name: "Kursname", 26 | background_color: "Hintergrundfarbe", 27 | choose_color: "Farbe wählen", 28 | preview: "Vorschau", 29 | edit_course_x: "Kurs {0} bearbeiten", 30 | delete_user: "Benutzer löschen", 31 | delete_pupil_or_teacher_confirm: "Folgenden {0} wirklich löschen?", 32 | create_pupil_accounts: "Schüleraccounts erstellen", 33 | same_password_for_all_accounts: "Dasselbe Passwort für alle Accounts", 34 | no_uniform_password_set: "Kein einheitliches Passwort festgelegt!", 35 | unique_passwords_are_missing: "Es fehlen einzelne Passwörter!", 36 | only_those_lines_accepted: "Nur Zeilen mit grünem Haken {0} werden berücksichtigt!", 37 | directly_add_pupils_to_courses: "Optional: Schüler direkt folgenden Gruppen zuweisen:", 38 | create_accounts: "Accounts erstellen", 39 | create_courses: "Kurse erstellen", 40 | search_users: "Personensuche", 41 | } -------------------------------------------------------------------------------- /frontend/src/locales/en/color_mode_switch.js: -------------------------------------------------------------------------------- 1 | export const color_mode_switch_en = { 2 | dark: "Dark", 3 | light: "Light", 4 | auto: "Auto", 5 | } -------------------------------------------------------------------------------- /frontend/src/locales/en/general.js: -------------------------------------------------------------------------------- 1 | export const general_en = { 2 | locale: "en", 3 | long_date_format: "E dd.MM.yyyy, HH:mm", // E is abbreviated weekday, e.g. "Mon" 4 | name: "Name", 5 | username: "Username", 6 | password: "Password", 7 | current_password: "Current password", 8 | new_password: "New password", 9 | confirm_password: "Confirm password", 10 | full_name: "Full name", 11 | input_incomplete: "Input incomplete", 12 | whitespace_in_username_not_allowed: "Username cannot contain whitespace!", 13 | passwords_not_identical: "Passwords are not identical", 14 | password_at_least_8: "Password must contain at least 8 characters!", 15 | password_doesnt_fullfill_requirements: "Password does not fulfill requirements!", 16 | password_invalid: "Password is invalid!", 17 | save_new_password: "Save new password", 18 | abort: "Abort", 19 | close: "Close", 20 | rename: "Rename", 21 | duplicate: "Duplicate", 22 | got_it: "Got it", 23 | save: "Save", 24 | save_changes: "Save changes", 25 | question_save_changes: "Save changes?", 26 | dont_save: "Don't save", 27 | delete: "Delete", 28 | download: "Download", 29 | download_project: "Download project", 30 | toastDownloadProjectError: "Error downloading project.", 31 | or: "or", 32 | more: "more", 33 | less: "less", 34 | select_date: "Select date...", 35 | input_missing: "Input missing", 36 | project_name: "Project name", 37 | class_name: "Java class name", 38 | project_description: "Project description", 39 | upload: "Upload", 40 | change_password: "Change password", 41 | back: "Back", 42 | continue: "Continue", 43 | course: "Course", 44 | courses: "Courses", 45 | database_id: "#id", 46 | role: "Role", 47 | teacher: "Teacher", 48 | pupil: "Pupil", 49 | settings: "Settings", 50 | edit_settings: "Edit settings", 51 | deadline: "Deadline", 52 | utc: "UTC", 53 | editing_time: "Editing time", 54 | days: "days", 55 | hours: "hours", 56 | seconds: "seconds", 57 | computation_time: "Computation time", 58 | computation_time_description: "Set the number of seconds of computation time (i.e. runtime) available on the server per action. An action is:
  • Compiling
  • Running
  • Testing
The default value is 5 seconds, which students cannot change in their own projects, as it could cause the server to be overwhelmed with endlessly running programs. However, it may be useful to increase the runtime for certain tasks, e.g. if a program needs to wait for user input, which also takes time.", 59 | at_least_3_default_5: "At least 3, default 5", 60 | result: "Result", 61 | open: "Open", 62 | error_changing_password: "Error changing password.", 63 | success_changing_password: "Password changed successfully.", 64 | toastRenameProjectSuccess: "Project has been renamed.", 65 | toastRenameProjectError: "Error renaming project.", 66 | add: "Add", 67 | restart: "Restart", 68 | restart_assignment: "Restart assignment", 69 | template: "Template", 70 | input: "Input", 71 | send: "Send", 72 | project: "Project", 73 | usermanagement: "User management", 74 | version: "Version", 75 | enable_tests: "Enable tests", 76 | enable_tests_question: "Enable tests?", 77 | enable_tests_description: "Should students have access to tests for this task? For this, the file Tests.java must contain meaningful JUnit tests. Otherwise, students will not see the test button and the teacher will of course not see an overview of the results at a glance.", 78 | use_test_functions: "Use test functions", 79 | activate_tests: "Activate tests", 80 | deactivate_tests: "Deactivate tests", 81 | } -------------------------------------------------------------------------------- /frontend/src/locales/en/home.js: -------------------------------------------------------------------------------- 1 | export const home_en = { 2 | home_title: "Home", 3 | new_project: "New project", 4 | toastStartHomeworkError: "Error starting the assignment. Please try again, or ask your teacher for help.", 5 | toastAddSolutionSuccess: "Solution added successfully.", 6 | toastAddSolutionError: "Error adding the solution.", 7 | toastDuplicateProjectSuccess: "Project duplicated successfully.", 8 | toastDuplicateProjectError: "Error duplicating the project.", 9 | toastRenameHomeworkSuccess: "Assignment renamed successfully.", 10 | toastRenameHomeworkError: "Error renaming the assignment.", 11 | toastDeleteProjectError: "Error deleting the project/assignment.", 12 | toastDeleteProjectSuccess: "The project or assignment has been deleted.", 13 | rename_assignment: "Rename assignment", 14 | enter_new_assignment_name: 'Enter the new name for assignment {0}:', 15 | rename_project: "Rename project", 16 | enter_new_project_name: 'Enter the new name for project {0}:', 17 | delete_private_project: "Delete private project?", 18 | want_to_delete_private_project: "Do you really want to delete your own private project titled {0}?", 19 | delete_private_project_teacher_warning: "If this project is linked as a solution, this link will also be deleted.", 20 | delete_progress: "Delete progress?", 21 | want_to_delete_progress: "You cannot completely delete an assignment. You can only delete your progress so far and start again from a \"clean\" project. {0}{1} Do you really want to delete your progress of {2}?", 22 | delete_assignment: "Delete assignment?", 23 | want_to_delete_assignment: "If you delete an assignment, it will also be deleted for all students. Do you really want to delete the assignment titled {0}?", 24 | add_solution: "Add solution", 25 | change_solution: "Change solution", 26 | add_solution_info: "Select a private project that students can view as a solution from a certain point in time. Students can only open and execute the solution, but not edit it.", 27 | select_the_solution: "Select the solution:", 28 | solution_visible_from_when: "From when should the solution be visible?", 29 | from_right_now: "From now on", 30 | visible_from_now: 'Visible from now on', 31 | visible_in_x_days_hours: "Visible in {0}, {1}", 32 | visible_in_x_days: '{n} days | {n} day | {n} days', // 0, 1 or more days 33 | visible_in_x_hours: '{n} hours | {n} hour | {n} hours', // 0, 1 or more hours 34 | add_solution_input_invalid: "Solution could not be saved. Please check your inputs.", 35 | project_search: "Project search", 36 | current_assignment: "Current assignments", 37 | previous_assignments: "Previous assignments", 38 | my_projects: "My projects", 39 | delete_solution: "Delete solution", 40 | delete_solution_confirm: "Do you really want to delete the link to the solution? Your private project will not be affected.", 41 | toastDeleteSolutionError: "Error deleting the solution.", 42 | toastDeleteSolutionSuccess: "The solution has been deleted.", 43 | filter_courses: "Filter courses", 44 | } -------------------------------------------------------------------------------- /frontend/src/locales/en/homework.js: -------------------------------------------------------------------------------- 1 | export const homework_en = { 2 | unknown_assignment: "Unknown assignment", 3 | error_on_changing_settings: "Error on changing settings.", 4 | settings_updated: "Settings updated.", 5 | cant_change_course_of_assignment: "You can not change the course of an already created assignment!
You can only delete the homework in Home and then create it again for a different course.", 6 | already_expired: "Already expired", 7 | show_results: "Show results", 8 | edit_template: "Edit template", 9 | number_of_compilations: "# Compilations", 10 | number_of_runs: "# Runs", 11 | number_of_tests: "# Tests", 12 | } -------------------------------------------------------------------------------- /frontend/src/locales/en/ide.js: -------------------------------------------------------------------------------- 1 | export const ide_en = { 2 | server_was_overloaded_try_again: "The server was probably overloaded 😥 Please try again!", 3 | no_live_output_result_to_follow: "Live output not possible, result will follow shortly...", 4 | compilation_successful: "Compilation successful 🎉", 5 | compilation_started: "Compilation started... 🛠", 6 | compilation_error: "Internal server-error while compiling! ❌", 7 | program_running: "Program running...", 8 | program_testing: "Program testing 📝➡️✅ please wait...", 9 | no_executables_found: "🔎 Unfortunately, no executables were found. Please compile first ⚙", 10 | program_exited_without_output: "Program exited (successfully, but without output)! ✔", 11 | program_exited_probably_with_error: "Program exited (probably with an error)! ❌", 12 | security_error: "💥🙈 There seems to have been a security error while testing your program. Apparently, your program tried to execute things that are not allowed. Please correct this first.\nIf the problem persists, you should contact your teacher.", 13 | computation_time_exceeded: "❌ Program was terminated prematurely! The computation time has probably expired. Do you have an infinite loop somewhere?", 14 | all_tests_passed: "All tests passed 🎉🤩\n\nYou can now try to \"beautify\" your source code ;-)", 15 | no_test_passed: "Oops 🧐 Apparently, not a single test was passed! All the tests shown above failed.", 16 | error_during_testing: "💥🙈 There seems to have been an error while testing your program. Please try again!\nFirst, make sure your program can be executed.\nIf the problem persists, you should contact your teacher.", 17 | test_aborted: "❌ The test was aborted by the user.", 18 | x_percent_of_tests_passed: "You passed {percent}% of the tests. The output above shows all the tests that have NOT been passed yet.", 19 | stopped_by_user: "\nProgram was stopped by the user! ✔", 20 | error_loading_project: "Error loading project. Please go back or reload.", 21 | error_on_saving: "Error on saving!", 22 | couldnt_rename_file: "File could not be renamed.", 23 | file_rename_successful: "File successfully renamed.", 24 | error_saving_project_description: "Error saving project description.", 25 | error_project_name_empty: "Project name must not be empty!", 26 | project_not_existing_or_no_access: "Project does not exist, or you do not have access to it!", 27 | error_creating_homework: "Error creating assignment!", 28 | homework_created_successfully: "Assignment created successfully.", 29 | file_created_successfully: "File created successfully.", 30 | error_creating_file: "Error creating file.", 31 | error_deleting_file: "Error deleting file.", 32 | error_reseting_homework_progress: "Error resetting assignment progress!", 33 | error_stopping_program: "Program could not be stopped!", 34 | changing_template_may_lead_to_inconsistencies: "Changing the template may lead to inconsistencies!", 35 | description_of_inconsistencies_when_changing_template: "You are currently editing the template of an assignment! If you make changes to the code and save them, this can lead to inconsistencies among the different students:
  • For students who have already started working on this assignment, your changes will not arrive until they delete their progress and start the assignment again.
  • For students who have not yet started working on this assignment, the first time they open it they will always use the latest code of this template.
Changes to the title or project description, on the other hand, are not a problem and can be changed at any time and will be updated for the students when they reload the IDE.", 36 | save_changes_before_closing: "Do you want to save the changes before closing?", 37 | add_new_class: "Add new class", 38 | description_new_file: "Enter the new name for the class. Remember that class names must always start with a capital letter and can only consist of letters and numbers (with a few exceptions)!", 39 | classname_is_invalid: "Classname is invalid or the name already exists!", 40 | question_delete_file: "Delete class?", 41 | ask_delete_class_x: "Do you really want to delete the class {0}?", 42 | delete_folder: "Delete folder", 43 | delete_folder_description: "Unfortunately, folders cannot be deleted directly. However, you can delete all the contents of the folder, and the folder will be deleted automatically.", 44 | rename_folder: "Rename folder", 45 | rename_folder_description: "Unfortunately, folders cannot be renamed directly. However, you can rename all the contents of the folder, and a new folder with the new name will be created automatically. The old folder will be deleted automatically.

Example:
If you have a folder named old containing two files named First.java and Second.java, with full names old/First.java and old/Second.java, renaming the files to new/First.java and new/Second.java is equivalent to renaming the folder from old to new.", 46 | rename_file: "Rename class", 47 | rename_file_description: "Enter the new class name for the class {0}.", 48 | rename_file_description_2: "Remember to adjust the content of the class to the new name afterwards!", 49 | rename_file_invalid: "Filename is invalid (it may contain spaces or special characters, or the name already exists)!", 50 | create_assignment: "Create assignment", 51 | create_assignment_description_1: "⚠️Important: The configuration of a project should be fully completed before creating an assignment from it. After this step, changes should be avoided, as students may otherwise work on different versions.", 52 | create_assignment_description_2: "When creating an assignment, the current state of your project is copied and saved as a template for the students in the background. After creation, further changes to this private project by you will not be visible in the assignment for the students. E.g. you can safely delete this private project afterwards, and the assignment will still exist. Important: Students cannot change or rename the entry point (= class with main method)!", 53 | assignment_already_created: "An assignment has already been created from this project for the following courses:", 54 | choose_course: "Choose course", 55 | create_course_in_usermanagement: "You must first create a course in the user management (ideally with students)!", 56 | create_assignment: "Create assignment", 57 | question_restart_assignment: "Restart assignment?", 58 | restart_assignment_description: "Do you want to delete your previous progress and start again from a \"clean\" project?", 59 | new_class: "New class", 60 | restart_assignment: "Restart assignment", 61 | keyboard_shortcuts: "Keyboard shortcuts", 62 | ctrl: "Ctrl", 63 | save_changes: "Save changes", 64 | compile_program: "Compile program", 65 | execute_program: "Execute program", 66 | test_program: "Test program", 67 | compile: "Compile", 68 | execute: "Execute", 69 | test: "Test", 70 | placeholder_for_input: "Input (press Enter to send)", 71 | set_as_entry_point: "Set as entry point", 72 | error_main_class_not_found: "No main method was found in the main/initial class (with 🏠). Make sure that a method with the following signature exists there:\npublic static void main(String[] args)", 73 | tooltip_entry_point: "The main/initial class of the program, which must also contain the main method.", 74 | edit_computation_time: "Edit computation time", 75 | success_setting_computation_time: "Computation time successfully set.", 76 | error_setting_computation_time: "Error setting computation time.", 77 | } -------------------------------------------------------------------------------- /frontend/src/locales/en/login.js: -------------------------------------------------------------------------------- 1 | export const login_en = { 2 | login: "Login", 3 | register: "Register", 4 | show_login_error: "Incorrect username or password", 5 | registration_only_for_teachers: "Registration only for teachers", 6 | teacher_key: "Teacher key", 7 | input_incomplete: "Input incomplete.", 8 | registration_successful: "Account created successfully. Please log in.", 9 | error_msg: "Error message", 10 | registration_error: "Account could not be created. The teacher key may be incorrect, the username already exists, or the username contains spaces.", 11 | token_expired: "Your session has expired. You need to log in again.", 12 | need_relogin_after_username_change: "You have to log in again after changing your username.", 13 | } -------------------------------------------------------------------------------- /frontend/src/locales/en/navbar.js: -------------------------------------------------------------------------------- 1 | export const navbar_en = { 2 | password_change_invalid: "Input invalid!
Make sure your old password is correct and that your new password fulfills the above criteria.", 3 | logout: "Logout", 4 | update_available: "Update available", 5 | update_available_text: "A new version of Schoco is available", 6 | current_version: "Current version", 7 | latest_version: "Latest version", 8 | new_version: "New version", 9 | current_version: "Current version", 10 | read_the_changelog: "Please read the {0} before updating!", 11 | changelog: "changelog", 12 | skip_version_warning: "You can disable the update information for the new version {0}. The update notice will then only be displayed again for the next higher version.", 13 | skip_version_x: "Skip version {0}", 14 | error_skipping_version: "Error skipping/hiding version {0}.", 15 | success_skipping_version: "Version {0} will no longer be displayed.", 16 | } -------------------------------------------------------------------------------- /frontend/src/locales/en/new_project.js: -------------------------------------------------------------------------------- 1 | export const new_project_en = { 2 | new_project_title: "New project", 3 | project_name_must_not_be_empty: "Project name must not be empty.", 4 | class_name_must_not_be_empty: "Class name must not be empty.", 5 | class_name_illegal: "Class name must start with a letter and must not contain a space.", 6 | project_could_not_be_created: "Project could not be created.", 7 | only_zip_files_allowed: "Only ZIP files are allowed.", 8 | file_too_large: "One of the selected files is too large (> 5 MB).", 9 | project_import_failed: "Not all projects could be imported!", 10 | project_import_success: "All projects have been imported! You can now find them in {0}.", 11 | new_empty_project: "1) New empty project", 12 | upload_zips: "2) Upload ZIPs", 13 | create_project: "Create project", 14 | upload_zips_description: "Upload previously exported projects. Multiple projects can be uploaded at once.", 15 | drop_files_to_upload: "Drop files to upload", 16 | selected_files: "Selected files:", 17 | reset_file_list: "Reset file list", 18 | classname_explanation: "Enter the name of the first Java class. The name should be based on the purpose of the project. The name must start with a capital letter and must not contain any special characters except numbers (with a few exceptions)." 19 | } -------------------------------------------------------------------------------- /frontend/src/locales/en/password_info.js: -------------------------------------------------------------------------------- 1 | export const password_info_en = { 2 | password_info: "Make sure the new password is at least 8 characters long and fulfills at least two of the following criteria:
  • Contains a letter
  • Contains a number
  • Contains a special character
", 3 | } -------------------------------------------------------------------------------- /frontend/src/locales/en/project_card.js: -------------------------------------------------------------------------------- 1 | export const project_card_en = { 2 | submit_until: "Submit until", 3 | begin_assignment: "Start assignment", 4 | edit_assignment: "Edit assignment", 5 | show_details: "Show details", 6 | open_project: "Open project", 7 | add_solution: "Add solution", 8 | open_solution: "Open solution", 9 | change_solution_line_break: "Change
solution", 10 | percent_solved: "Solved {0} %.", 11 | pupils_editing: "Edited by {0} pupils.", 12 | } -------------------------------------------------------------------------------- /frontend/src/locales/en/users.js: -------------------------------------------------------------------------------- 1 | export const users_en = { 2 | error_confirming_password: "Error confirming password!", 3 | name_must_not_be_empty: "Name must not be empty!", 4 | password_changed_success: "Password changed successfully.", 5 | password_changed_error: "Error changing password.", 6 | error_on_changing_username: "Error changing username. Maybe it already exists.", 7 | couldnt_delete_user: "User {0} {1} could not be deleted.", 8 | delete_user_success: "User {0} {1} has been successfully deleted.", 9 | course_created_success: "Course created.", 10 | course_created_error: "Error creating course. Maybe the course name already exists?", 11 | course_edited_success: "Course edited.", 12 | course_edited_error: "Error editing course.", 13 | course_deleted_success: "Course deleted.", 14 | course_deleted_error: "Error deleting course.", 15 | add_user_to_course_error: "Could not add user to course. Is the user already a member?", 16 | all_accounts_created: "All accounts have been created.", 17 | x_accounts_created: "{0} accounts have been created.", 18 | following_accounts_not_created: "The following accounts could not be created. Maybe they already exist?", 19 | following_accounts_not_created_password_info: "There may have been problems with the passwords:", 20 | add_users_to_courses_error: "There were errors in assigning students to courses. Please check the assignment manually.", 21 | delete_course: "Delete course", 22 | delete_course_confirm: "Do you really want to delete the course {0}?", 23 | delete_course_confirm_info: "This will also delete homework assignments associated with this course. In addition, all students will be removed from the course.", 24 | create_new_course: "Create new course", 25 | course_name: "Course name", 26 | background_color: "Background color", 27 | choose_color: "Choose color", 28 | preview: "Preview", 29 | edit_course_x: "Edit course {0}", 30 | delete_user: "Delete user", 31 | delete_pupil_or_teacher_confirm: "Do you really want to delete the following {0}?", 32 | create_pupil_accounts: "Create student accounts", 33 | same_password_for_all_accounts: "Same password for all accounts", 34 | no_uniform_password_set: "No uniform password set!", 35 | unique_passwords_are_missing: "Individual passwords are missing!", 36 | only_those_lines_accepted: "Only lines with green checkmark {0} will be accepted!", 37 | directly_add_pupils_to_courses: "Optional: Directly add students to courses:", 38 | create_accounts: "Create accounts", 39 | create_courses: "Create courses", 40 | search_users: "Search users", 41 | } -------------------------------------------------------------------------------- /frontend/src/locales/index.js: -------------------------------------------------------------------------------- 1 | // german 2 | import {general_de} from "./de/general.js" 3 | import {home_de} from "./de/home.js" 4 | import {navbar_de} from "./de/navbar.js" 5 | import {login_de} from "./de/login.js" 6 | import {new_project_de} from "./de/new_project.js" 7 | import {users_de} from "./de/users.js" 8 | import {homework_de} from "./de/homework.js" 9 | import { color_mode_switch_de } from "./de/color_mode_switch.js" 10 | import { password_info_de } from "./de/password_info.js" 11 | import { project_card_de } from "./de/project_card.js" 12 | import { ide_de } from "./de/ide.js" 13 | 14 | // english 15 | import {general_en} from "./en/general.js" 16 | import {home_en} from "./en/home.js" 17 | import {navbar_en} from "./en/navbar.js" 18 | import {login_en} from "./en/login.js" 19 | import {new_project_en} from "./en/new_project.js" 20 | import {users_en} from "./en/users.js" 21 | import {homework_en} from "./en/homework.js" 22 | import {color_mode_switch_en} from "./en/color_mode_switch.js" 23 | import { password_info_en } from "./en/password_info.js" 24 | import { project_card_en } from "./en/project_card.js" 25 | import {ide_en} from "./en/ide.js" 26 | 27 | const de = { 28 | ...general_de, 29 | ...home_de, 30 | ...navbar_de, 31 | ...login_de, 32 | ...new_project_de, 33 | ...users_de, 34 | ...homework_de, 35 | ...color_mode_switch_de, 36 | ...password_info_de, 37 | ...project_card_de, 38 | ...ide_de, 39 | } 40 | 41 | const en = { 42 | ...general_en, 43 | ...home_en, 44 | ...navbar_en, 45 | ...login_en, 46 | ...new_project_en, 47 | ...users_en, 48 | ...homework_en, 49 | ...color_mode_switch_en, 50 | ...password_info_en, 51 | ...project_card_en, 52 | ...ide_en, 53 | } 54 | 55 | 56 | export const defaultLocale = 'en' 57 | 58 | export const languages = { 59 | de: de, 60 | en: en, 61 | } -------------------------------------------------------------------------------- /frontend/src/main.js: -------------------------------------------------------------------------------- 1 | import { createApp, markRaw } from 'vue' 2 | import { createPinia } from 'pinia' 3 | import { router } from './router' 4 | import App from './App.vue' 5 | import "bootstrap" 6 | import { VAceEditor } from 'vue3-ace-editor'; 7 | import { library } from '@fortawesome/fontawesome-svg-core' 8 | import { FontAwesomeIcon, FontAwesomeLayers } from '@fortawesome/vue-fontawesome' 9 | import { faUser, faUsers, faKey, faRightFromBracket, faHammer, faPlay, faEyeSlash, faEye, faLock, faSignature, faUserPlus, faCheck, faCircle, faPlus, faXmark, faTrash, faFileCirclePlus, faFolderPlus, faFloppyDisk, faArrowRotateLeft, faArrowRotateRight, faGear, faCirclePlay, faListCheck, faFolderOpen, faFile, faPencil, faShareNodes, faCircleQuestion, faSquare, faSquareCheck, faBars, faCopy, faDownload, faUpload, faListUl, faCircleCheck, faTriangleExclamation, faSearch, faCircleHalfStroke, faSun, faMoon, faCode, faCircleExclamation, faBan, faArrowRight, faHouse, faStopwatch, faKeyboard } from '@fortawesome/free-solid-svg-icons' 10 | import { createI18n } from 'vue-i18n' 11 | import { languages } from './locales/index.js' 12 | const messages = Object.assign(languages) 13 | 14 | library.add(faUser, faUsers, faKey, faRightFromBracket, faHammer, faPlay, faEyeSlash, faEye, faLock, faSignature, faUserPlus, faCheck, faCircle, faPlus, faXmark, faTrash, faFileCirclePlus, faFolderPlus, faFloppyDisk, faArrowRotateLeft, faArrowRotateRight, faGear, faCirclePlay, faListCheck, faFolderOpen, faFile, faPencil, faShareNodes, faCircleQuestion, faSquare, faSquareCheck, faBars, faCopy, faDownload, faUpload, faListUl, faCircleCheck, faTriangleExclamation, faSearch, faCircleHalfStroke, faSun, faMoon, faCode, faCircleExclamation, faBan, faArrowRight, faHouse, faStopwatch, faKeyboard) 15 | 16 | const pinia = createPinia() 17 | pinia.use(({ store }) => { 18 | store.$router = markRaw(router) 19 | }) 20 | 21 | 22 | const i18n = createI18n({ 23 | legacy: false, 24 | locale: navigator.language, 25 | fallbackLocale: 'en', 26 | messages 27 | }) 28 | 29 | //component('VueDatePicker', VueDatePicker) 30 | createApp(App).use(pinia).use(router).use(i18n).component('v-ace-editor', VAceEditor).component('font-awesome-icon', FontAwesomeIcon).component('font-awesome-layers', FontAwesomeLayers).mount('#app') 31 | 32 | -------------------------------------------------------------------------------- /frontend/src/router/index.js: -------------------------------------------------------------------------------- 1 | import { createRouter, createWebHashHistory } from 'vue-router' 2 | import { useAuthStore } from '../stores/auth.store.js' 3 | import Login from "../views/Login.vue" 4 | import Home from "../views/Home.vue" 5 | import Users from "../views/Users.vue" 6 | import NewProject from "../views/NewProject.vue" 7 | import Ide from "../views/IDE.vue" 8 | import Homework from "../views/Homework.vue" 9 | import ChangePassword from "../views/ChangePassword.vue" 10 | 11 | 12 | const routes = [ 13 | { path: '/login', name: 'login', component: Login }, 14 | { path: '/home', name: "home", component: Home }, 15 | { path: '/users', name: "users", component: Users }, 16 | { path: '/newProject', name: "newProject", component: NewProject }, 17 | { path: '/ide/:project_uuid/:user_id', name: "ide", component: Ide, params: { project_uuid: "", user_id: "" } }, 18 | { path: '/homework/:id', name: "homework", component: Homework, params: { id: "" } }, 19 | { path: '/changePassword', name: "changePassword", component: ChangePassword}, 20 | { path: '/:pathMatch(.*)*', redirect: "/home" } 21 | ] 22 | 23 | export const router = createRouter({ 24 | history: createWebHashHistory(), 25 | routes 26 | }) 27 | 28 | router.beforeEach(async (to) => { 29 | const publicPages = ['/login'] 30 | const authRequired = !publicPages.includes(to.path) 31 | const auth = useAuthStore() 32 | 33 | const teacherPages = ['/users', '/homework'] 34 | const teacherRequired = teacherPages.includes(to.path) 35 | 36 | if (authRequired && !auth.user) { 37 | auth.returnUrl = to.fullPath; 38 | return '/login' 39 | } 40 | 41 | if (teacherRequired && (!auth.user || !auth.isTeacher())) { 42 | auth.returnUrl = '/home' 43 | return '/home' 44 | } 45 | 46 | if (to.path == '/login' && auth.user) { 47 | return '/home' 48 | } 49 | }) 50 | 51 | //export default router -------------------------------------------------------------------------------- /frontend/src/sass/main.scss: -------------------------------------------------------------------------------- 1 | 2 | $primary: #6427ea; 3 | 4 | 5 | $enable-dark-mode: true; 6 | $enable-shadows: true; 7 | 8 | .modal { 9 | @extend [data-bs-theme="light"]; 10 | color: #212529; 11 | } 12 | 13 | .popover { 14 | @extend [data-bs-theme="light"]; 15 | color: #212529; 16 | } 17 | 18 | 19 | 20 | //import bootstrap 21 | @import "../../node_modules/bootstrap/scss/bootstrap"; -------------------------------------------------------------------------------- /frontend/src/services/auth-headers.js: -------------------------------------------------------------------------------- 1 | import { useAuthStore } from "../stores/auth.store.js" 2 | 3 | export default function authHeader(url) { 4 | const { user } = useAuthStore() 5 | const isLoggedIn = !!user?.access_token 6 | const isApiUrl = url.startsWith(import.meta.env.VITE_API_URL) 7 | 8 | if (isLoggedIn && isApiUrl) { 9 | return { Authorization: 'Bearer ' + user.access_token } 10 | } else { 11 | return {} 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /frontend/src/services/code.service.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | import { useAuthStore } from '../stores/auth.store.js'; 3 | import jwt_decode from 'jwt-decode' 4 | 5 | const API_URL = import.meta.env.VITE_API_URL 6 | 7 | const axiosAuth = axios.create({ 8 | baseURL: API_URL 9 | }); 10 | axiosAuth.interceptors.request.use((config) => { 11 | const authStore = useAuthStore() 12 | if (authStore.user) { 13 | // go to login if token has expired 14 | let decoded = jwt_decode(authStore.user.access_token) 15 | if (decoded.exp < Date.now() / 1000) { 16 | authStore.logout_token_expired() 17 | return Promise.reject('Token expired') 18 | } 19 | 20 | const token = authStore.user.access_token 21 | if (token) { 22 | config.headers['Authorization'] = `Bearer ${token}` 23 | } 24 | return config 25 | } 26 | }, (error) => { 27 | return Promise.reject(error) 28 | }) 29 | 30 | class CodeService { 31 | createNewHelloWorld(helloWorldName, className, helloWorldDescription, language) { 32 | return axiosAuth.post('createNewHelloWorld', { 'projectName': helloWorldName, 'className': className, 'projectDescription': helloWorldDescription, 'language': language }) 33 | } 34 | 35 | loadAllFiles(project_uuid, user_id) { 36 | return axiosAuth.get(`loadAllFiles/${project_uuid}/${user_id}`) 37 | } 38 | 39 | getProjectInfo(project_uuid, user_id) { 40 | return axiosAuth.get(`getProjectInfo/${project_uuid}/${user_id}`) 41 | } 42 | 43 | updateDescription(project_uuid, user_id, description) { 44 | return axiosAuth.post(`updateDescription/${project_uuid}/${user_id}`, { 'text': description }) 45 | } 46 | 47 | updateProjectName(project_uuid, user_id, projectName) { 48 | return axiosAuth.post(`updateProjectName/${project_uuid}/${user_id}`, { 'text': projectName }) 49 | } 50 | 51 | saveFileChanges(changes, project_uuid, user_id) { 52 | return axiosAuth.post(`saveFileChanges/${project_uuid}/${user_id}`, { 'changes': changes }) 53 | } 54 | 55 | getProjectsAsTeacher() { 56 | return axiosAuth.get('getProjectsAsTeacher') 57 | } 58 | 59 | getProjectsAsPupil() { 60 | return axiosAuth.get('getProjectsAsPupil') 61 | } 62 | 63 | startCompile(projectFiles, project_uuid, user_id) { 64 | return axiosAuth.post(`startCompile/${project_uuid}/${user_id}`, { 'files': projectFiles }) 65 | } 66 | 67 | prepareExecute(project_uuid, user_id) { 68 | return axiosAuth.get(`prepareExecute/${project_uuid}/${user_id}`) 69 | } 70 | 71 | startExecute(ip, port, container_uuid, project_uuid, user_id, save_output) { 72 | return axiosAuth.post(`startExecute/${project_uuid}/${user_id}`, { 'ip': ip, 'port': port, 'container_uuid': container_uuid, 'save_output': save_output }) 73 | } 74 | 75 | prepareTest(project_uuid, user_id) { 76 | return axiosAuth.get(`prepareTest/${project_uuid}/${user_id}`) 77 | } 78 | 79 | startTest(ip, port, container_uuid, project_uuid, user_id) { 80 | return axiosAuth.post(`startTest/${project_uuid}/${user_id}`, { 'ip': ip, 'port': port, 'container_uuid': container_uuid }) 81 | } 82 | 83 | createHomework(orig_project_uuid, project_files, course_id, deadlineDate, computationTime, enableTests) { 84 | return axiosAuth.post(`createHomework/${orig_project_uuid}`, { 'files': project_files, 'course_id': course_id, 'deadline_date': deadlineDate, 'computation_time': computationTime, 'enable_tests': enableTests }) 85 | } 86 | 87 | startHomework(id) { 88 | return axiosAuth.post('startHomework', { 'id': id }) 89 | } 90 | 91 | getHomeworkInfo(id) { 92 | return axiosAuth.post('getHomeworkInfo', { 'id': id }) 93 | } 94 | 95 | deleteProject(uuid, user_id) { 96 | return axiosAuth.post(`deleteProject/${uuid}`, { 'user_id': user_id }) 97 | } 98 | 99 | deleteHomework(id) { 100 | return axiosAuth.post('deleteHomework', { 'id': id }) 101 | } 102 | 103 | renameFile(project_uuid, user_id, oldName, newName, fileContent, sha) { 104 | return axiosAuth.post(`renameFile/${project_uuid}/${user_id}`, { 'old_path': oldName, 'new_path': newName, 'content': fileContent, 'sha': sha }) 105 | } 106 | 107 | renameHomework(id, newName) { 108 | return axiosAuth.post('renameHomework', { 'id': id, 'new_name': newName }) 109 | } 110 | 111 | renameProject(uuid, new_name) { 112 | return axiosAuth.post(`renameProject/${uuid}`, { 'text': new_name }) 113 | } 114 | 115 | duplicateProject(uuid) { 116 | return axiosAuth.post(`duplicateProject/${uuid}`) 117 | } 118 | 119 | downloadProject(uuid) { 120 | return axiosAuth.get(`downloadProject/${uuid}`, { responseType: 'blob' }) 121 | } 122 | 123 | uploadProject(file, config) { 124 | const formData = new FormData(); 125 | formData.append('file', file); 126 | return axiosAuth.postForm('uploadProject', file, config) 127 | } 128 | 129 | addNewClass(project_uuid, user_id, className, language) { 130 | return axiosAuth.post(`addNewClass/${project_uuid}/${user_id}`, { 'className': className, 'language': language }) 131 | } 132 | 133 | deleteFile(project_uuid, user_id, path, sha) { 134 | return axiosAuth.post(`deleteFile/${project_uuid}/${user_id}`, { 'path': path, 'sha': sha }) 135 | } 136 | 137 | updateHomeworkSettings(id, deadlineDate, computationTime, enableTests) { 138 | return axiosAuth.post('updateHomeworkSettings', { 'id': id, 'deadline_date': deadlineDate, 'computation_time': computationTime, 'enable_tests': enableTests }) 139 | } 140 | 141 | stopContainer(uuid) { 142 | return axiosAuth.post('stopContainer', {'uuid': uuid}) 143 | } 144 | 145 | addSolution(homework_id, solution_id, solution_start_showing) { 146 | return axiosAuth.post('addSolution', {'homework_id': homework_id, 'solution_id': solution_id, 'solution_start_showing': solution_start_showing}) 147 | } 148 | 149 | deleteSolution(homework_id) { 150 | return axiosAuth.post('deleteSolution', {'homework_id': homework_id}) 151 | } 152 | 153 | loadEntryPoint(project_uuid, user_id) { 154 | return axiosAuth.get(`loadEntryPoint/${project_uuid}/${user_id}`) 155 | } 156 | 157 | setEntryPoint(project_uuid, user_id, path) { 158 | return axiosAuth.post(`setEntryPoint/${project_uuid}/${user_id}`, {'entry_point': path}) 159 | } 160 | 161 | getTeacherComputationTime(project_uuid, user_id) { 162 | return axiosAuth.get(`getTeacherComputationTime/${project_uuid}/${user_id}`) 163 | } 164 | 165 | setTeacherComputationTime(computation_time, project_uuid, user_id) { 166 | return axiosAuth.post(`setTeacherComputationTime/${project_uuid}/${user_id}`, {'computation_time': computation_time}) 167 | } 168 | } 169 | 170 | export default new CodeService() -------------------------------------------------------------------------------- /frontend/src/services/user.service.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | import { useAuthStore } from '../stores/auth.store.js'; 3 | import jwt_decode from 'jwt-decode' 4 | 5 | const API_URL = import.meta.env.VITE_API_URL 6 | 7 | const axiosAuth = axios.create({ 8 | baseURL: API_URL 9 | }); 10 | axiosAuth.interceptors.request.use((config) => { 11 | const authStore = useAuthStore() 12 | if (authStore.user) { 13 | // go to login if token has expired 14 | let decoded = jwt_decode(authStore.user.access_token) 15 | if (decoded.exp < Date.now() / 1000) { 16 | authStore.logout_token_expired() 17 | return Promise.reject('Token expired') 18 | } 19 | 20 | const token = authStore.user.access_token 21 | if (token) { 22 | config.headers['Authorization'] = `Bearer ${token}` 23 | } 24 | return config 25 | } 26 | }, (error) => { 27 | return Promise.reject(error) 28 | }) 29 | 30 | 31 | class UserService { 32 | registerTeacher(teacherkey, name, username, password) { 33 | var bodyFormData = new FormData() 34 | bodyFormData.append("teacherkey", teacherkey); 35 | bodyFormData.append("full_name", name); 36 | bodyFormData.append("username", username); 37 | bodyFormData.append("password", password); 38 | return axios.post(API_URL + 'registerTeacher', bodyFormData) 39 | } 40 | 41 | registerPupils(newPupils, courseIDs) { 42 | return axiosAuth.post('registerPupils', { 'newPupils': newPupils, 'courseIDs': courseIDs }) 43 | } 44 | 45 | setNewPassword(username, password) { 46 | return axiosAuth.post('setNewPassword', { 'username': username, 'password': password }) 47 | } 48 | 49 | changePassword(oldPassword, newPassword) { 50 | return axiosAuth.post('changePassword', { 'oldPassword': oldPassword, 'newPassword': newPassword }) 51 | } 52 | 53 | getAllUsers() { 54 | return axiosAuth.get('getAllUsers') 55 | } 56 | 57 | getAllCourses() { 58 | return axiosAuth.get('getAllCourses') 59 | } 60 | 61 | addNewCourse(courseName, courseColor, courseFontDark) { 62 | return axiosAuth.post('addNewCourse', { 'name': courseName, 'color': courseColor, 'fontDark': courseFontDark }) 63 | } 64 | 65 | editCourse(courseID, courseName, courseColor, courseFontDark) { 66 | return axiosAuth.post('editCourse', { 'id': courseID, 'name': courseName, 'color': courseColor, 'fontDark': courseFontDark }) 67 | } 68 | 69 | removeCourse(course_id) { 70 | return axiosAuth.post('removeCourse', { 'id': course_id }) 71 | } 72 | 73 | addCourseToUser(user_id, coursename) { 74 | return axiosAuth.post('addCourseToUser', { 'user_id': user_id, 'coursename': coursename }) 75 | } 76 | 77 | removeCourseFromUser(user_id, course_id) { 78 | return axiosAuth.post('removeCourseFromUser', { 'user_id': user_id, 'course_id': course_id }) 79 | } 80 | 81 | deleteUser(user_id) { 82 | return axiosAuth.post('deleteUser', { 'user_id': user_id }) 83 | } 84 | 85 | changeName(user_id, name) { 86 | return axiosAuth.post('changeName', { 'user_id': user_id, 'name': name }) 87 | } 88 | 89 | changeUsername(user_id, username) { 90 | return axiosAuth.post('changeUsername', { 'user_id': user_id, 'name': username }) 91 | } 92 | 93 | checkExistingHomework(uuid) { 94 | return axiosAuth.post('checkExistingHomework', { 'uuid': uuid }) 95 | } 96 | 97 | confirmTeacherPassword(password) { 98 | return axiosAuth.post('confirmTeacherPassword', { 'password': password }) 99 | } 100 | 101 | getLatestVersion() { 102 | return axiosAuth.get('getLatestVersion') 103 | } 104 | 105 | skipLatestVersion(skip_version) { 106 | return axiosAuth.post('skipLatestVersion', { 'skip_version': skip_version }) 107 | } 108 | } 109 | 110 | export default new UserService() -------------------------------------------------------------------------------- /frontend/src/services/websocket-worker.js: -------------------------------------------------------------------------------- 1 | let dec = new TextDecoder(); 2 | 3 | onmessage = function (event) { 4 | postMessage(dec.decode(event.data)); 5 | }; -------------------------------------------------------------------------------- /frontend/src/stores/auth.store.js: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia' 2 | import axios from 'axios' 3 | 4 | const API_URL = import.meta.env.VITE_API_URL 5 | 6 | export const useAuthStore = defineStore({ 7 | id: 'auth', 8 | state: () => ({ 9 | 10 | // initialize state from local storage to enable user to stay logged in 11 | user: JSON.parse(localStorage.getItem('user')), 12 | returnUrl: null 13 | 14 | }), 15 | actions: { 16 | async login(username, password) { 17 | var bodyFormData = new FormData() 18 | bodyFormData.append('username', username) 19 | bodyFormData.append('password', password) 20 | try { 21 | const response = await axios.post(API_URL + 'login', bodyFormData) 22 | if (response.data.access_token) { 23 | this.user = response.data 24 | localStorage.setItem('user', JSON.stringify(response.data)) 25 | } 26 | } catch (err) { 27 | return err 28 | } 29 | 30 | if (this.user.must_change_password) { 31 | this.$router.push('/changePassword'); 32 | } else { 33 | this.$router.push(this.returnUrl || '/home'); 34 | } 35 | return 36 | }, 37 | logout() { 38 | this.user = null; 39 | this.$router.push('/login'); 40 | localStorage.removeItem('user'); 41 | }, 42 | logout_username_changed() { 43 | this.user = null; 44 | localStorage.removeItem('user'); 45 | this.$router.push({ path: '/login', query: { username_changed: true } }); 46 | }, 47 | logout_token_expired() { 48 | this.user = null; 49 | localStorage.removeItem('user'); 50 | this.$router.push({ path: '/login', query: { token_expired: true } }); 51 | }, 52 | isTeacher() { 53 | return (this.user && this.user.role == 'teacher') 54 | }, 55 | setPasswordChanged() { 56 | this.user.must_change_password = false 57 | localStorage.setItem('user', JSON.stringify(this.user)) 58 | } 59 | } 60 | }); -------------------------------------------------------------------------------- /frontend/src/views/ChangePassword.vue: -------------------------------------------------------------------------------- 1 | 80 | 81 | 142 | 143 | -------------------------------------------------------------------------------- /frontend/vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import vue from '@vitejs/plugin-vue' 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [vue()], 7 | css: { 8 | preprocessorOptions: { 9 | scss: { 10 | additionalData: `@import "./src/sass/main.scss";` 11 | } 12 | } 13 | } 14 | }); 15 | -------------------------------------------------------------------------------- /gitea/.gitignore: -------------------------------------------------------------------------------- 1 | /data -------------------------------------------------------------------------------- /gitea/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | 3 | networks: 4 | gitea: 5 | external: false 6 | 7 | services: 8 | server: 9 | image: gitea/gitea:1.17.3 10 | container_name: gitea 11 | environment: 12 | - USER_UID=1000 13 | - USER_GID=1000 14 | - GITEA__security__INSTALL_LOCK=true 15 | restart: always 16 | networks: 17 | - gitea 18 | volumes: 19 | - ./data:/data 20 | - /etc/timezone:/etc/timezone:ro 21 | - /etc/localtime:/etc/localtime:ro 22 | ports: 23 | - "3000:3000" 24 | - "222:22" 25 | -------------------------------------------------------------------------------- /logos/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PhiTux/schoco/c302238b5c42b8125eff5d3bfd2d4a2485f14f69/logos/favicon.ico -------------------------------------------------------------------------------- /logos/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PhiTux/schoco/c302238b5c42b8125eff5d3bfd2d4a2485f14f69/logos/favicon.png -------------------------------------------------------------------------------- /logos/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 105 | -------------------------------------------------------------------------------- /logos/schoco-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 100 | -------------------------------------------------------------------------------- /logos/schoco-text-1.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 14 | 24 | 26 | 31 | 35 | 39 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /logos/schoco-text-2.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 14 | 24 | 26 | 31 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /logos/schoco-text-3.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 14 | 24 | 26 | 31 | 35 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /logos/schoco-text.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /nginx/nginx.conf: -------------------------------------------------------------------------------- 1 | #worker_processes 1; 2 | #user root; 3 | #user nginx; 4 | 5 | #events { 6 | # worker_connections 1024; 7 | #} 8 | 9 | #http { 10 | include /etc/nginx/mime.types; 11 | 12 | upstream docker { 13 | server unix:/var/run/docker.sock; 14 | } 15 | 16 | server { 17 | listen 8080; 18 | server_name _; 19 | 20 | #access_log /dev/stdout; 21 | #error_log /dev/stdout info; 22 | #access_log /tmp/access.log; 23 | #error_log /tmp/error.log info; 24 | 25 | root /usr/share/nginx/html; 26 | index index.html index.html; 27 | 28 | # redirect WS to docker.sock 29 | # match the URL: /containers/{id}/attach... 30 | location ~ ^/(containers/[0-9a-f]+/attach.*) { 31 | proxy_pass http://docker/$1$is_args$args; 32 | proxy_http_version 1.1; 33 | proxy_set_header Upgrade $http_upgrade; 34 | proxy_set_header Connection "Upgrade"; 35 | proxy_set_header Host $host; 36 | 37 | proxy_set_header X-Real-IP $remote_addr; 38 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 39 | proxy_set_header X-Frame-Options SAMEORIGIN; 40 | } 41 | 42 | location /api/ { 43 | client_max_body_size 128M; 44 | 45 | #proxy_pass http://127.0.0.1:80; 46 | proxy_pass http://schoco-backend:80; 47 | proxy_http_version 1.1; 48 | proxy_redirect default; 49 | proxy_set_header Upgrade $http_upgrade; 50 | proxy_set_header Connection "upgrade"; 51 | proxy_set_header Host $host; 52 | proxy_set_header X-Real-IP $remote_addr; 53 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 54 | proxy_set_header X-Forwarded-Host $server_name; 55 | 56 | # adapt this for future zip-Import of projects... 57 | location /api/importData { 58 | client_max_body_size 0; 59 | proxy_pass http://127.0.0.1:5000; 60 | } 61 | } 62 | 63 | location / { 64 | try_files $uri /index.html =404; 65 | } 66 | } 67 | #} 68 | -------------------------------------------------------------------------------- /nginx/nginx.dev.conf: -------------------------------------------------------------------------------- 1 | 2 | include /etc/nginx/mime.types; 3 | 4 | upstream docker { 5 | server unix:/var/run/docker.sock; 6 | } 7 | 8 | # Server for redirecting WS to docker.sock 9 | server { 10 | listen 8080; 11 | server_name _; 12 | 13 | root /usr/share/nginx/html; 14 | index index.html index.html; 15 | 16 | # match the URL: /containers/{id}/attach... 17 | location ~ ^/(containers/[0-9a-f]+/attach.*) { 18 | proxy_pass http://docker/$1$is_args$args; 19 | proxy_http_version 1.1; 20 | proxy_set_header Upgrade $http_upgrade; 21 | proxy_set_header Connection "Upgrade"; 22 | proxy_set_header Host $host; 23 | 24 | proxy_set_header X-Real-IP $remote_addr; 25 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 26 | proxy_set_header X-Frame-Options SAMEORIGIN; 27 | } 28 | 29 | location / { 30 | try_files $uri /index.html =404; 31 | } 32 | } 33 | #} 34 | -------------------------------------------------------------------------------- /readme/IDE.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PhiTux/schoco/c302238b5c42b8125eff5d3bfd2d4a2485f14f69/readme/IDE.png -------------------------------------------------------------------------------- /readme/assignment_teacher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PhiTux/schoco/c302238b5c42b8125eff5d3bfd2d4a2485f14f69/readme/assignment_teacher.png -------------------------------------------------------------------------------- /readme/home_assignment_teacher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PhiTux/schoco/c302238b5c42b8125eff5d3bfd2d4a2485f14f69/readme/home_assignment_teacher.png -------------------------------------------------------------------------------- /readme/schoco-full.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PhiTux/schoco/c302238b5c42b8125eff5d3bfd2d4a2485f14f69/readme/schoco-full.png -------------------------------------------------------------------------------- /readme/schoco-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 100 | -------------------------------------------------------------------------------- /readme/schoco-promo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PhiTux/schoco/c302238b5c42b8125eff5d3bfd2d4a2485f14f69/readme/schoco-promo.jpg -------------------------------------------------------------------------------- /readme/usermanagement_course.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PhiTux/schoco/c302238b5c42b8125eff5d3bfd2d4a2485f14f69/readme/usermanagement_course.jpg -------------------------------------------------------------------------------- /readme/usermanagement_end.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PhiTux/schoco/c302238b5c42b8125eff5d3bfd2d4a2485f14f69/readme/usermanagement_end.jpg -------------------------------------------------------------------------------- /readme/usermanagement_students.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PhiTux/schoco/c302238b5c42b8125eff5d3bfd2d4a2485f14f69/readme/usermanagement_students.jpg --------------------------------------------------------------------------------