Compare commits
	
		
			103 Commits
		
	
	
		
			1.1.5
			...
			f2207e8667
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| f2207e8667 | |||
| 5ee4748e42 | |||
| 5dd11bbe6c | |||
| 25390e2132 | |||
| bc7d6e5146 | |||
| bfd301e3a5 | |||
| 9bdb14896b | |||
| f40afe84ed | |||
| 3c1d5ac6c7 | |||
| 302e1c7307 | |||
| 22ba5d5f7f | |||
| b2a429cc48 | |||
| 1a768e1188 | |||
| b78d28a6d3 | |||
| 954e8ec359 | |||
| a5d13629e5 | |||
| a3b3c40d77 | |||
| 0f59fb51b4 | |||
| 97f32069fa | |||
| 9f8ca744bc | |||
| 14b212dcf8 | |||
| be415a8ee4 | |||
| 7f7aaa180a | |||
| 3e17a788d7 | |||
| fb838f5a32 | |||
| 7581dc058c | |||
| a212539366 | |||
| dc6c41578a | |||
| a39da6deba | |||
| d457627e00 | |||
| 795ddc516d | |||
| e6f09c11ba | |||
| f7c8958d20 | |||
| 006e962bf1 | |||
| f49e600613 | |||
| 940a30e9aa | |||
| d863f4fb1e | |||
| dc57a0d0c1 | |||
| 5ba9dfc263 | |||
| ca8efdaa85 | |||
| 8e3af696e0 | |||
| fb6291792d | |||
| 993b8f6a71 | |||
| f9cc9b5e14 | |||
| 460d1ee131 | |||
| 90847a2730 | |||
| a292b45fcb | |||
| 22f17d10e0 | |||
| ae9bf2a562 | |||
| 6394a7c880 | |||
| 5ab5e850bd | |||
| a97a464a83 | |||
| 2b06efeece | |||
| 024f2b04ce | |||
| bf71d62dc5 | |||
| 7dce2c6cfd | |||
| a1dc5656b8 | |||
| dd4c5259d2 | |||
| 884aab08ed | |||
| c64c8779e3 | |||
| 1d614a06c4 | |||
| 3f10c8afaa | |||
| 9de1e295dd | |||
| 6287d4582d | |||
| 7dfa0fc6c4 | |||
| a53b2b8fc1 | |||
| 36972c9322 | |||
| 7bf2920d17 | |||
| c30306c163 | |||
| 46c9a61124 | |||
| 36acf1a00a | |||
| f9029bf1c3 | |||
| fe72c02562 | |||
| 8d9fcd3d7c | |||
| ec7a0a7a64 | |||
| efd8f9f9f5 | |||
| e2dce9fab3 | |||
| bbac0d3688 | |||
| 6114077591 | |||
| 678a97f140 | |||
| c22f752788 | |||
| 1798054580 | |||
| 9a48b1a859 | |||
| 2f1f65eba0 | |||
| be95af43c2 | |||
| f20279a4b4 | |||
| 3aa27dff4a | |||
| f01916b363 | |||
| a13203c477 | |||
| 2bb6bc6534 | |||
| d0d5e4b6d1 | |||
| 16cb89ca38 | |||
| b428840df7 | |||
| 4ca79e6540 | |||
| 9dd875fcb3 | |||
| e2169ba8ab | |||
| 697f5e3167 | |||
| dd064d63af | |||
| 3177be1bf0 | |||
| db57059727 | |||
| 132cf2df0e | |||
| 283cc96e48 | |||
| 238947a894 | 
| @ -1,8 +0,0 @@ | |||||||
| kind: pipeline |  | ||||||
| name: default |  | ||||||
|  |  | ||||||
| steps: |  | ||||||
| - name: test |  | ||||||
|   image: gradle:jdk11 |  | ||||||
|   commands: |  | ||||||
|   - gradle test |  | ||||||
							
								
								
									
										6
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										6
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @ -35,6 +35,7 @@ captures/ | |||||||
|  |  | ||||||
| # Intellij | # Intellij | ||||||
| *.iml | *.iml | ||||||
|  | .idea/ | ||||||
| .idea/workspace.xml | .idea/workspace.xml | ||||||
| .idea/tasks.xml | .idea/tasks.xml | ||||||
| .idea/gradle.xml | .idea/gradle.xml | ||||||
| @ -55,6 +56,9 @@ freeline.py | |||||||
| freeline/ | freeline/ | ||||||
| freeline_project_description.json | freeline_project_description.json | ||||||
|  |  | ||||||
|  | # KDE | ||||||
| .directory | .directory | ||||||
| .idea/ |  | ||||||
|  | # tcor | ||||||
|  | tcor/ | ||||||
|  |  | ||||||
|  | |||||||
							
								
								
									
										21
									
								
								.woodpecker.yml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								.woodpecker.yml
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,21 @@ | |||||||
|  | pipeline: | ||||||
|  |   test: | ||||||
|  |     image: gradle:jdk11 | ||||||
|  |     commands: | ||||||
|  |       - gradle test | ||||||
|  |   build: | ||||||
|  |     image: gradle:jdk11 | ||||||
|  |     commands: | ||||||
|  |       - gradle bootJar | ||||||
|  |     when: | ||||||
|  |       event: | ||||||
|  |         - tag | ||||||
|  |   docker: | ||||||
|  |     image: techknowlogick/drone-docker | ||||||
|  |     privileged: true | ||||||
|  |     repo: mosadxyz/tcor | ||||||
|  |     secrets: [docker_username, docker_password] | ||||||
|  |     tags: latest | ||||||
|  |     when: | ||||||
|  |       event: | ||||||
|  |         - tag | ||||||
							
								
								
									
										9
									
								
								Dockerfile
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								Dockerfile
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,9 @@ | |||||||
|  | FROM eclipse-temurin:21-jre-alpine | ||||||
|  | RUN addgroup -S spring && adduser -S spring -G spring | ||||||
|  | #RUN groupadd -r spring && useradd -r -g spring spring # for openjdk:xx builds | ||||||
|  | RUN mkdir /tcor && chown spring:spring /tcor | ||||||
|  | USER spring:spring | ||||||
|  | ARG JAR_FILE=build/libs/*.jar | ||||||
|  | COPY ${JAR_FILE} thecitadelofricks.jar | ||||||
|  | ENTRYPOINT ["java","-Djavax.net.ssl.trustStore=/tcor/cacerts", "-Djavax.net.ssl.trustStorePassword=changeit", "-jar","/thecitadelofricks.jar"] | ||||||
|  | VOLUME /tcor | ||||||
| @ -1,4 +1,6 @@ | |||||||
| [](https://drone.mosad.xyz/Seil0/TheCitadelofRicks) |  | ||||||
|  | [](https://ci.mosad.xyz/Seil0/TheCitadelofRicks) | ||||||
|  | [](https://git.mosad.xyz/Seil0/TheCitadelofRicks/releases) | ||||||
| [](https://www.gnu.org/licenses/gpl-3.0) | [](https://www.gnu.org/licenses/gpl-3.0) | ||||||
| # TheCitadelofRicks | # TheCitadelofRicks | ||||||
|  |  | ||||||
|  | |||||||
							
								
								
									
										68
									
								
								build.gradle
									
									
									
									
									
								
							
							
						
						
									
										68
									
								
								build.gradle
									
									
									
									
									
								
							| @ -1,51 +1,45 @@ | |||||||
|  | plugins { | ||||||
| buildscript { |     id 'org.jetbrains.kotlin.jvm' version '1.9.23' | ||||||
|     ext.kotlin_version = '1.3.50' |     id 'org.jetbrains.kotlin.plugin.spring' version '1.9.23' | ||||||
|     ext.spring_boot_version = '2.1.9.RELEASE' |     id 'org.springframework.boot' version '3.2.4' | ||||||
|  |     id 'io.spring.dependency-management' version '1.1.4' | ||||||
|     repositories { |  | ||||||
|         jcenter() |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     dependencies { |  | ||||||
|         classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" |  | ||||||
|         classpath "org.jetbrains.kotlin:kotlin-allopen:$kotlin_version" |  | ||||||
|         classpath "org.springframework.boot:spring-boot-gradle-plugin:$spring_boot_version" |  | ||||||
|     } |  | ||||||
| } | } | ||||||
|  |  | ||||||
| apply plugin: 'kotlin' | group 'org.mosad' | ||||||
| apply plugin: 'kotlin-spring' | version '1.3.1' | ||||||
| apply plugin: 'org.springframework.boot' |  | ||||||
| apply plugin: 'io.spring.dependency-management' | repositories { | ||||||
|  |     mavenCentral() | ||||||
|  | } | ||||||
|  |  | ||||||
|  | dependencies { | ||||||
|  |     implementation 'org.springframework.boot:spring-boot-starter-web:3.2.4' | ||||||
|  |     implementation 'org.jetbrains.kotlin:kotlin-stdlib:1.9.23' | ||||||
|  |     implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.0' | ||||||
|  |     implementation 'org.jsoup:jsoup:1.17.2' | ||||||
|  |     implementation 'com.google.code.gson:gson:2.10.1' | ||||||
|  |  | ||||||
|  |     testImplementation 'org.junit.jupiter:junit-jupiter:5.10.2' | ||||||
|  | } | ||||||
|  |  | ||||||
| test { | test { | ||||||
|     useJUnitPlatform() |     useJUnitPlatform() | ||||||
|  |  | ||||||
|     testLogging { |     testLogging { | ||||||
|         events "PASSED", "FAILED", "SKIPPED" |         events 'PASSED', 'FAILED', 'SKIPPED' | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
| repositories { | def jvmTargetVersion = "21" | ||||||
|     jcenter() |  | ||||||
| } |  | ||||||
|  |  | ||||||
| dependencies { |  | ||||||
|     implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" |  | ||||||
|     implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.1" |  | ||||||
|     implementation 'org.jsoup:jsoup:1.12.1' |  | ||||||
|     implementation 'org.springframework.boot:spring-boot-starter-web' |  | ||||||
|  |  | ||||||
|     testImplementation("org.junit.jupiter:junit-jupiter:5.5.1") |  | ||||||
| } |  | ||||||
|  |  | ||||||
| compileKotlin { | compileKotlin { | ||||||
|     kotlinOptions.jvmTarget = "1.8" |     kotlinOptions.jvmTarget = jvmTargetVersion | ||||||
|  | } | ||||||
|  | compileJava { | ||||||
|  |     targetCompatibility = jvmTargetVersion | ||||||
| } | } | ||||||
| compileTestKotlin { | compileTestKotlin { | ||||||
|     kotlinOptions.jvmTarget = "1.8" |     kotlinOptions.jvmTarget = jvmTargetVersion | ||||||
|  | } | ||||||
|  | compileTestJava { | ||||||
|  |     targetCompatibility = jvmTargetVersion | ||||||
| } | } | ||||||
|  |  | ||||||
| group 'org.mosad' |  | ||||||
| version '1.1.4' |  | ||||||
|  | |||||||
							
								
								
									
										
											BIN
										
									
								
								gradle/wrapper/gradle-wrapper.jar
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										
											BIN
										
									
								
								gradle/wrapper/gradle-wrapper.jar
									
									
									
									
										vendored
									
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										3
									
								
								gradle/wrapper/gradle-wrapper.properties
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										3
									
								
								gradle/wrapper/gradle-wrapper.properties
									
									
									
									
										vendored
									
									
								
							| @ -1,5 +1,6 @@ | |||||||
| distributionBase=GRADLE_USER_HOME | distributionBase=GRADLE_USER_HOME | ||||||
| distributionPath=wrapper/dists | distributionPath=wrapper/dists | ||||||
| distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.2-bin.zip | distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip | ||||||
|  | networkTimeout=10000 | ||||||
| zipStoreBase=GRADLE_USER_HOME | zipStoreBase=GRADLE_USER_HOME | ||||||
| zipStorePath=wrapper/dists | zipStorePath=wrapper/dists | ||||||
|  | |||||||
							
								
								
									
										286
									
								
								gradlew
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										286
									
								
								gradlew
									
									
									
									
										vendored
									
									
								
							| @ -1,7 +1,7 @@ | |||||||
| #!/usr/bin/env sh | #!/bin/sh | ||||||
|  |  | ||||||
| # | # | ||||||
| # Copyright 2015 the original author or authors. | # Copyright © 2015-2021 the original authors. | ||||||
| # | # | ||||||
| # Licensed under the Apache License, Version 2.0 (the "License"); | # Licensed under the Apache License, Version 2.0 (the "License"); | ||||||
| # you may not use this file except in compliance with the License. | # you may not use this file except in compliance with the License. | ||||||
| @ -17,78 +17,113 @@ | |||||||
| # | # | ||||||
|  |  | ||||||
| ############################################################################## | ############################################################################## | ||||||
| ## | # | ||||||
| ##  Gradle start up script for UN*X | #   Gradle start up script for POSIX generated by Gradle. | ||||||
| ## | # | ||||||
|  | #   Important for running: | ||||||
|  | # | ||||||
|  | #   (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is | ||||||
|  | #       noncompliant, but you have some other compliant shell such as ksh or | ||||||
|  | #       bash, then to run this script, type that shell name before the whole | ||||||
|  | #       command line, like: | ||||||
|  | # | ||||||
|  | #           ksh Gradle | ||||||
|  | # | ||||||
|  | #       Busybox and similar reduced shells will NOT work, because this script | ||||||
|  | #       requires all of these POSIX shell features: | ||||||
|  | #         * functions; | ||||||
|  | #         * expansions «$var», «${var}», «${var:-default}», «${var+SET}», | ||||||
|  | #           «${var#prefix}», «${var%suffix}», and «$( cmd )»; | ||||||
|  | #         * compound commands having a testable exit status, especially «case»; | ||||||
|  | #         * various built-in commands including «command», «set», and «ulimit». | ||||||
|  | # | ||||||
|  | #   Important for patching: | ||||||
|  | # | ||||||
|  | #   (2) This script targets any POSIX shell, so it avoids extensions provided | ||||||
|  | #       by Bash, Ksh, etc; in particular arrays are avoided. | ||||||
|  | # | ||||||
|  | #       The "traditional" practice of packing multiple parameters into a | ||||||
|  | #       space-separated string is a well documented source of bugs and security | ||||||
|  | #       problems, so this is (mostly) avoided, by progressively accumulating | ||||||
|  | #       options in "$@", and eventually passing that to Java. | ||||||
|  | # | ||||||
|  | #       Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, | ||||||
|  | #       and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; | ||||||
|  | #       see the in-line comments for details. | ||||||
|  | # | ||||||
|  | #       There are tweaks for specific operating systems such as AIX, CygWin, | ||||||
|  | #       Darwin, MinGW, and NonStop. | ||||||
|  | # | ||||||
|  | #   (3) This script is generated from the Groovy template | ||||||
|  | #       https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt | ||||||
|  | #       within the Gradle project. | ||||||
|  | # | ||||||
|  | #       You can find Gradle at https://github.com/gradle/gradle/. | ||||||
|  | # | ||||||
| ############################################################################## | ############################################################################## | ||||||
|  |  | ||||||
| # Attempt to set APP_HOME | # Attempt to set APP_HOME | ||||||
| # Resolve links: $0 may be a link |  | ||||||
| PRG="$0" |  | ||||||
| # Need this for relative symlinks. |  | ||||||
| while [ -h "$PRG" ] ; do |  | ||||||
|     ls=`ls -ld "$PRG"` |  | ||||||
|     link=`expr "$ls" : '.*-> \(.*\)$'` |  | ||||||
|     if expr "$link" : '/.*' > /dev/null; then |  | ||||||
|         PRG="$link" |  | ||||||
|     else |  | ||||||
|         PRG=`dirname "$PRG"`"/$link" |  | ||||||
|     fi |  | ||||||
| done |  | ||||||
| SAVED="`pwd`" |  | ||||||
| cd "`dirname \"$PRG\"`/" >/dev/null |  | ||||||
| APP_HOME="`pwd -P`" |  | ||||||
| cd "$SAVED" >/dev/null |  | ||||||
|  |  | ||||||
| APP_NAME="Gradle" | # Resolve links: $0 may be a link | ||||||
| APP_BASE_NAME=`basename "$0"` | app_path=$0 | ||||||
|  |  | ||||||
|  | # Need this for daisy-chained symlinks. | ||||||
|  | while | ||||||
|  |     APP_HOME=${app_path%"${app_path##*/}"}  # leaves a trailing /; empty if no leading path | ||||||
|  |     [ -h "$app_path" ] | ||||||
|  | do | ||||||
|  |     ls=$( ls -ld "$app_path" ) | ||||||
|  |     link=${ls#*' -> '} | ||||||
|  |     case $link in             #( | ||||||
|  |       /*)   app_path=$link ;; #( | ||||||
|  |       *)    app_path=$APP_HOME$link ;; | ||||||
|  |     esac | ||||||
|  | done | ||||||
|  |  | ||||||
|  | # This is normally unused | ||||||
|  | # shellcheck disable=SC2034 | ||||||
|  | APP_BASE_NAME=${0##*/} | ||||||
|  | APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit | ||||||
|  |  | ||||||
| # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. | ||||||
| DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' | ||||||
|  |  | ||||||
| # Use the maximum available, or set MAX_FD != -1 to use that value. | # Use the maximum available, or set MAX_FD != -1 to use that value. | ||||||
| MAX_FD="maximum" | MAX_FD=maximum | ||||||
|  |  | ||||||
| warn () { | warn () { | ||||||
|     echo "$*" |     echo "$*" | ||||||
| } | } >&2 | ||||||
|  |  | ||||||
| die () { | die () { | ||||||
|     echo |     echo | ||||||
|     echo "$*" |     echo "$*" | ||||||
|     echo |     echo | ||||||
|     exit 1 |     exit 1 | ||||||
| } | } >&2 | ||||||
|  |  | ||||||
| # OS specific support (must be 'true' or 'false'). | # OS specific support (must be 'true' or 'false'). | ||||||
| cygwin=false | cygwin=false | ||||||
| msys=false | msys=false | ||||||
| darwin=false | darwin=false | ||||||
| nonstop=false | nonstop=false | ||||||
| case "`uname`" in | case "$( uname )" in                #( | ||||||
|   CYGWIN* ) |   CYGWIN* )         cygwin=true  ;; #( | ||||||
|     cygwin=true |   Darwin* )         darwin=true  ;; #( | ||||||
|     ;; |   MSYS* | MINGW* )  msys=true    ;; #( | ||||||
|   Darwin* ) |   NONSTOP* )        nonstop=true ;; | ||||||
|     darwin=true |  | ||||||
|     ;; |  | ||||||
|   MINGW* ) |  | ||||||
|     msys=true |  | ||||||
|     ;; |  | ||||||
|   NONSTOP* ) |  | ||||||
|     nonstop=true |  | ||||||
|     ;; |  | ||||||
| esac | esac | ||||||
|  |  | ||||||
| CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar | ||||||
|  |  | ||||||
|  |  | ||||||
| # Determine the Java command to use to start the JVM. | # Determine the Java command to use to start the JVM. | ||||||
| if [ -n "$JAVA_HOME" ] ; then | if [ -n "$JAVA_HOME" ] ; then | ||||||
|     if [ -x "$JAVA_HOME/jre/sh/java" ] ; then |     if [ -x "$JAVA_HOME/jre/sh/java" ] ; then | ||||||
|         # IBM's JDK on AIX uses strange locations for the executables |         # IBM's JDK on AIX uses strange locations for the executables | ||||||
|         JAVACMD="$JAVA_HOME/jre/sh/java" |         JAVACMD=$JAVA_HOME/jre/sh/java | ||||||
|     else |     else | ||||||
|         JAVACMD="$JAVA_HOME/bin/java" |         JAVACMD=$JAVA_HOME/bin/java | ||||||
|     fi |     fi | ||||||
|     if [ ! -x "$JAVACMD" ] ; then |     if [ ! -x "$JAVACMD" ] ; then | ||||||
|         die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME |         die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME | ||||||
| @ -97,7 +132,7 @@ Please set the JAVA_HOME variable in your environment to match the | |||||||
| location of your Java installation." | location of your Java installation." | ||||||
|     fi |     fi | ||||||
| else | else | ||||||
|     JAVACMD="java" |     JAVACMD=java | ||||||
|     which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. |     which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. | ||||||
|  |  | ||||||
| Please set the JAVA_HOME variable in your environment to match the | Please set the JAVA_HOME variable in your environment to match the | ||||||
| @ -105,84 +140,105 @@ location of your Java installation." | |||||||
| fi | fi | ||||||
|  |  | ||||||
| # Increase the maximum file descriptors if we can. | # Increase the maximum file descriptors if we can. | ||||||
| if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then | ||||||
|     MAX_FD_LIMIT=`ulimit -H -n` |     case $MAX_FD in #( | ||||||
|     if [ $? -eq 0 ] ; then |       max*) | ||||||
|         if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then |         # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. | ||||||
|             MAX_FD="$MAX_FD_LIMIT" |         # shellcheck disable=SC3045  | ||||||
|         fi |         MAX_FD=$( ulimit -H -n ) || | ||||||
|         ulimit -n $MAX_FD |             warn "Could not query maximum file descriptor limit" | ||||||
|         if [ $? -ne 0 ] ; then |     esac | ||||||
|             warn "Could not set maximum file descriptor limit: $MAX_FD" |     case $MAX_FD in  #( | ||||||
|         fi |       '' | soft) :;; #( | ||||||
|     else |       *) | ||||||
|         warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" |         # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. | ||||||
|     fi |         # shellcheck disable=SC3045  | ||||||
| fi |         ulimit -n "$MAX_FD" || | ||||||
|  |             warn "Could not set maximum file descriptor limit to $MAX_FD" | ||||||
| # For Darwin, add options to specify how the application appears in the dock |  | ||||||
| if $darwin; then |  | ||||||
|     GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" |  | ||||||
| fi |  | ||||||
|  |  | ||||||
| # For Cygwin or MSYS, switch paths to Windows format before running java |  | ||||||
| if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then |  | ||||||
|     APP_HOME=`cygpath --path --mixed "$APP_HOME"` |  | ||||||
|     CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` |  | ||||||
|     JAVACMD=`cygpath --unix "$JAVACMD"` |  | ||||||
|  |  | ||||||
|     # We build the pattern for arguments to be converted via cygpath |  | ||||||
|     ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` |  | ||||||
|     SEP="" |  | ||||||
|     for dir in $ROOTDIRSRAW ; do |  | ||||||
|         ROOTDIRS="$ROOTDIRS$SEP$dir" |  | ||||||
|         SEP="|" |  | ||||||
|     done |  | ||||||
|     OURCYGPATTERN="(^($ROOTDIRS))" |  | ||||||
|     # Add a user-defined pattern to the cygpath arguments |  | ||||||
|     if [ "$GRADLE_CYGPATTERN" != "" ] ; then |  | ||||||
|         OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" |  | ||||||
|     fi |  | ||||||
|     # Now convert the arguments - kludge to limit ourselves to /bin/sh |  | ||||||
|     i=0 |  | ||||||
|     for arg in "$@" ; do |  | ||||||
|         CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` |  | ||||||
|         CHECK2=`echo "$arg"|egrep -c "^-"`                                 ### Determine if an option |  | ||||||
|  |  | ||||||
|         if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then                    ### Added a condition |  | ||||||
|             eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` |  | ||||||
|         else |  | ||||||
|             eval `echo args$i`="\"$arg\"" |  | ||||||
|         fi |  | ||||||
|         i=$((i+1)) |  | ||||||
|     done |  | ||||||
|     case $i in |  | ||||||
|         (0) set -- ;; |  | ||||||
|         (1) set -- "$args0" ;; |  | ||||||
|         (2) set -- "$args0" "$args1" ;; |  | ||||||
|         (3) set -- "$args0" "$args1" "$args2" ;; |  | ||||||
|         (4) set -- "$args0" "$args1" "$args2" "$args3" ;; |  | ||||||
|         (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; |  | ||||||
|         (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; |  | ||||||
|         (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; |  | ||||||
|         (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; |  | ||||||
|         (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; |  | ||||||
|     esac |     esac | ||||||
| fi | fi | ||||||
|  |  | ||||||
| # Escape application args | # Collect all arguments for the java command, stacking in reverse order: | ||||||
| save () { | #   * args from the command line | ||||||
|     for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done | #   * the main class name | ||||||
|     echo " " | #   * -classpath | ||||||
| } | #   * -D...appname settings | ||||||
| APP_ARGS=$(save "$@") | #   * --module-path (only if needed) | ||||||
|  | #   * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. | ||||||
|  |  | ||||||
| # Collect all arguments for the java command, following the shell quoting and substitution rules | # For Cygwin or MSYS, switch paths to Windows format before running java | ||||||
| eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" | if "$cygwin" || "$msys" ; then | ||||||
|  |     APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) | ||||||
|  |     CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) | ||||||
|  |  | ||||||
| # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong |     JAVACMD=$( cygpath --unix "$JAVACMD" ) | ||||||
| if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then |  | ||||||
|   cd "$(dirname "$0")" |     # Now convert the arguments - kludge to limit ourselves to /bin/sh | ||||||
|  |     for arg do | ||||||
|  |         if | ||||||
|  |             case $arg in                                #( | ||||||
|  |               -*)   false ;;                            # don't mess with options #( | ||||||
|  |               /?*)  t=${arg#/} t=/${t%%/*}              # looks like a POSIX filepath | ||||||
|  |                     [ -e "$t" ] ;;                      #( | ||||||
|  |               *)    false ;; | ||||||
|  |             esac | ||||||
|  |         then | ||||||
|  |             arg=$( cygpath --path --ignore --mixed "$arg" ) | ||||||
|  |         fi | ||||||
|  |         # Roll the args list around exactly as many times as the number of | ||||||
|  |         # args, so each arg winds up back in the position where it started, but | ||||||
|  |         # possibly modified. | ||||||
|  |         # | ||||||
|  |         # NB: a `for` loop captures its iteration list before it begins, so | ||||||
|  |         # changing the positional parameters here affects neither the number of | ||||||
|  |         # iterations, nor the values presented in `arg`. | ||||||
|  |         shift                   # remove old arg | ||||||
|  |         set -- "$@" "$arg"      # push replacement arg | ||||||
|  |     done | ||||||
| fi | fi | ||||||
|  |  | ||||||
|  | # Collect all arguments for the java command; | ||||||
|  | #   * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of | ||||||
|  | #     shell script including quotes and variable substitutions, so put them in | ||||||
|  | #     double quotes to make sure that they get re-expanded; and | ||||||
|  | #   * put everything else in single quotes, so that it's not re-expanded. | ||||||
|  |  | ||||||
|  | set -- \ | ||||||
|  |         "-Dorg.gradle.appname=$APP_BASE_NAME" \ | ||||||
|  |         -classpath "$CLASSPATH" \ | ||||||
|  |         org.gradle.wrapper.GradleWrapperMain \ | ||||||
|  |         "$@" | ||||||
|  |  | ||||||
|  | # Stop when "xargs" is not available. | ||||||
|  | if ! command -v xargs >/dev/null 2>&1 | ||||||
|  | then | ||||||
|  |     die "xargs is not available" | ||||||
|  | fi | ||||||
|  |  | ||||||
|  | # Use "xargs" to parse quoted args. | ||||||
|  | # | ||||||
|  | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. | ||||||
|  | # | ||||||
|  | # In Bash we could simply go: | ||||||
|  | # | ||||||
|  | #   readarray ARGS < <( xargs -n1 <<<"$var" ) && | ||||||
|  | #   set -- "${ARGS[@]}" "$@" | ||||||
|  | # | ||||||
|  | # but POSIX shell has neither arrays nor command substitution, so instead we | ||||||
|  | # post-process each arg (as a line of input to sed) to backslash-escape any | ||||||
|  | # character that might be a shell metacharacter, then use eval to reverse | ||||||
|  | # that process (while maintaining the separation between arguments), and wrap | ||||||
|  | # the whole thing up as a single "set" statement. | ||||||
|  | # | ||||||
|  | # This will of course break if any of these variables contains a newline or | ||||||
|  | # an unmatched quote. | ||||||
|  | # | ||||||
|  |  | ||||||
|  | eval "set -- $( | ||||||
|  |         printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | | ||||||
|  |         xargs -n1 | | ||||||
|  |         sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | | ||||||
|  |         tr '\n' ' ' | ||||||
|  |     )" '"$@"' | ||||||
|  |  | ||||||
| exec "$JAVACMD" "$@" | exec "$JAVACMD" "$@" | ||||||
|  | |||||||
							
								
								
									
										38
									
								
								gradlew.bat
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										38
									
								
								gradlew.bat
									
									
									
									
										vendored
									
									
								
							| @ -14,7 +14,7 @@ | |||||||
| @rem limitations under the License. | @rem limitations under the License. | ||||||
| @rem | @rem | ||||||
|  |  | ||||||
| @if "%DEBUG%" == "" @echo off | @if "%DEBUG%"=="" @echo off | ||||||
| @rem ########################################################################## | @rem ########################################################################## | ||||||
| @rem | @rem | ||||||
| @rem  Gradle startup script for Windows | @rem  Gradle startup script for Windows | ||||||
| @ -25,10 +25,14 @@ | |||||||
| if "%OS%"=="Windows_NT" setlocal | if "%OS%"=="Windows_NT" setlocal | ||||||
|  |  | ||||||
| set DIRNAME=%~dp0 | set DIRNAME=%~dp0 | ||||||
| if "%DIRNAME%" == "" set DIRNAME=. | if "%DIRNAME%"=="" set DIRNAME=. | ||||||
|  | @rem This is normally unused | ||||||
| set APP_BASE_NAME=%~n0 | set APP_BASE_NAME=%~n0 | ||||||
| set APP_HOME=%DIRNAME% | set APP_HOME=%DIRNAME% | ||||||
|  |  | ||||||
|  | @rem Resolve any "." and ".." in APP_HOME to make it shorter. | ||||||
|  | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi | ||||||
|  |  | ||||||
| @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. | ||||||
| set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" | ||||||
|  |  | ||||||
| @ -37,7 +41,7 @@ if defined JAVA_HOME goto findJavaFromJavaHome | |||||||
|  |  | ||||||
| set JAVA_EXE=java.exe | set JAVA_EXE=java.exe | ||||||
| %JAVA_EXE% -version >NUL 2>&1 | %JAVA_EXE% -version >NUL 2>&1 | ||||||
| if "%ERRORLEVEL%" == "0" goto init | if %ERRORLEVEL% equ 0 goto execute | ||||||
|  |  | ||||||
| echo. | echo. | ||||||
| echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. | ||||||
| @ -51,7 +55,7 @@ goto fail | |||||||
| set JAVA_HOME=%JAVA_HOME:"=% | set JAVA_HOME=%JAVA_HOME:"=% | ||||||
| set JAVA_EXE=%JAVA_HOME%/bin/java.exe | set JAVA_EXE=%JAVA_HOME%/bin/java.exe | ||||||
|  |  | ||||||
| if exist "%JAVA_EXE%" goto init | if exist "%JAVA_EXE%" goto execute | ||||||
|  |  | ||||||
| echo. | echo. | ||||||
| echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% | ||||||
| @ -61,38 +65,26 @@ echo location of your Java installation. | |||||||
|  |  | ||||||
| goto fail | goto fail | ||||||
|  |  | ||||||
| :init |  | ||||||
| @rem Get command-line arguments, handling Windows variants |  | ||||||
|  |  | ||||||
| if not "%OS%" == "Windows_NT" goto win9xME_args |  | ||||||
|  |  | ||||||
| :win9xME_args |  | ||||||
| @rem Slurp the command line arguments. |  | ||||||
| set CMD_LINE_ARGS= |  | ||||||
| set _SKIP=2 |  | ||||||
|  |  | ||||||
| :win9xME_args_slurp |  | ||||||
| if "x%~1" == "x" goto execute |  | ||||||
|  |  | ||||||
| set CMD_LINE_ARGS=%* |  | ||||||
|  |  | ||||||
| :execute | :execute | ||||||
| @rem Setup the command line | @rem Setup the command line | ||||||
|  |  | ||||||
| set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar | ||||||
|  |  | ||||||
|  |  | ||||||
| @rem Execute Gradle | @rem Execute Gradle | ||||||
| "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* | ||||||
|  |  | ||||||
| :end | :end | ||||||
| @rem End local scope for the variables with windows NT shell | @rem End local scope for the variables with windows NT shell | ||||||
| if "%ERRORLEVEL%"=="0" goto mainEnd | if %ERRORLEVEL% equ 0 goto mainEnd | ||||||
|  |  | ||||||
| :fail | :fail | ||||||
| rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of | ||||||
| rem the _cmd.exe /c_ return code! | rem the _cmd.exe /c_ return code! | ||||||
| if  not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 | set EXIT_CODE=%ERRORLEVEL% | ||||||
| exit /b 1 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 | ||||||
|  | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% | ||||||
|  | exit /b %EXIT_CODE% | ||||||
|  |  | ||||||
| :mainEnd | :mainEnd | ||||||
| if "%OS%"=="Windows_NT" endlocal | if "%OS%"=="Windows_NT" endlocal | ||||||
|  | |||||||
| @ -1,7 +1,7 @@ | |||||||
| /** | /** | ||||||
|  * TheCitadelofRicks |  * TheCitadelofRicks | ||||||
|  * |  * | ||||||
|  * Copyright 2019  <seil0@mosad.xyz> |  * Copyright 2019-2020  <seil0@mosad.xyz> | ||||||
|  * |  * | ||||||
|  * This program is free software; you can redistribute it and/or modify |  * This program is free software; you can redistribute it and/or modify | ||||||
|  * it under the terms of the GNU General Public License as published by |  * it under the terms of the GNU General Public License as published by | ||||||
| @ -22,190 +22,140 @@ | |||||||
|  |  | ||||||
| package org.mosad.thecitadelofricks | package org.mosad.thecitadelofricks | ||||||
|  |  | ||||||
| import org.mosad.thecitadelofricks.CacheController.Companion.courseList | import org.mosad.thecitadelofricks.controller.CacheController | ||||||
| import org.mosad.thecitadelofricks.CacheController.Companion.mensaMenu | import org.mosad.thecitadelofricks.controller.CacheController.Companion.courseList | ||||||
| import org.mosad.thecitadelofricks.CacheController.Companion.timetableList | import org.mosad.thecitadelofricks.controller.CacheController.Companion.getLesson | ||||||
|  | import org.mosad.thecitadelofricks.controller.CacheController.Companion.getLessonSubjectList | ||||||
|  | import org.mosad.thecitadelofricks.controller.CacheController.Companion.getRoomSchedule | ||||||
|  | import org.mosad.thecitadelofricks.controller.CacheController.Companion.getTimetable | ||||||
|  | import org.mosad.thecitadelofricks.controller.CacheController.Companion.mensaMenu | ||||||
|  | import org.mosad.thecitadelofricks.controller.CacheController.Companion.roomList | ||||||
|  | import org.mosad.thecitadelofricks.controller.StartupController | ||||||
|  | import org.mosad.thecitadelofricks.controller.StatusController.Companion.getStatus | ||||||
|  | import org.mosad.thecitadelofricks.controller.StatusController.Companion.updateCourseListRequests | ||||||
|  | import org.mosad.thecitadelofricks.controller.StatusController.Companion.updateMensaMenuRequests | ||||||
|  | import org.mosad.thecitadelofricks.controller.StatusController.Companion.updateRoomListRequests | ||||||
|  | import org.mosad.thecitadelofricks.controller.StatusController.Companion.updateRoomScheduleRequests | ||||||
|  | import org.mosad.thecitadelofricks.controller.StatusController.Companion.updateTimetableRequests | ||||||
| import org.slf4j.Logger | import org.slf4j.Logger | ||||||
| import org.slf4j.LoggerFactory | import org.slf4j.LoggerFactory | ||||||
| import org.springframework.web.bind.annotation.RequestMapping | import org.springframework.web.bind.annotation.RequestMapping | ||||||
| import org.springframework.web.bind.annotation.RequestParam | import org.springframework.web.bind.annotation.RequestParam | ||||||
| import org.springframework.web.bind.annotation.RestController | import org.springframework.web.bind.annotation.RestController | ||||||
| import java.net.HttpURLConnection |  | ||||||
| import java.net.URL |  | ||||||
| import java.time.LocalDateTime | import java.time.LocalDateTime | ||||||
| import java.util.* | import java.util.* | ||||||
| import kotlin.collections.ArrayList | import kotlin.collections.ArrayList | ||||||
| import kotlin.collections.HashSet |  | ||||||
|  |  | ||||||
| @RestController | @RestController | ||||||
| class APIController { | class APIController { | ||||||
|  |  | ||||||
|     private val logger: Logger = LoggerFactory.getLogger(APIController::class.java) |     private val logger: Logger = LoggerFactory.getLogger(APIController::class.java) | ||||||
|     private val cache = CacheController() |  | ||||||
|  |  | ||||||
|     private val apiVersion = "1.1.3" |     companion object { | ||||||
|     private val softwareVersion = "1.1.5" |         const val apiVersion = "1.4.0" | ||||||
|     private val startTime = System.currentTimeMillis() / 1000 |         const val softwareVersion = "1.3.1" | ||||||
|  |         val startTime = System.currentTimeMillis() / 1000 | ||||||
|  |     } | ||||||
|  |  | ||||||
|     private var totalRequests = 0 |     init { | ||||||
|     private var mensaMenuRequests = 0 |         StartupController() | ||||||
|     private var timetableRequests = ArrayList<TimetableCounter>() |         CacheController() | ||||||
|  |  | ||||||
|     @Deprecated("courses is replaced by courseList", replaceWith = ReplaceWith("courseList()")) |  | ||||||
|     @RequestMapping("/courses") |  | ||||||
|     fun courses(): CourseList { |  | ||||||
|         return courseList() |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     @RequestMapping("/courseList") |     @RequestMapping("/courseList") | ||||||
|     fun courseList(): CourseList { |     fun courseList(): CoursesListRet { | ||||||
|         logger.info("courseList request at ${LocalDateTime.now()}!") |         logger.info("courseList request at ${LocalDateTime.now()}!") | ||||||
|         totalRequests++ |         updateCourseListRequests() | ||||||
|         return courseList |  | ||||||
|  |         return CoursesListRet(courseList.meta, ArrayList(courseList.courses.values)) | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     @RequestMapping("/mensamenu") |     @RequestMapping("/mensamenu") | ||||||
|     fun mensamenu(): MensaMenu { |     fun mensamenu(): MensaMenu { | ||||||
|         logger.info("mensamenu request at ${LocalDateTime.now()}!") |         logger.info("mensamenu request at ${LocalDateTime.now()}!") | ||||||
|         mensaMenuRequests++ |         updateMensaMenuRequests() | ||||||
|         totalRequests++ |  | ||||||
|         return mensaMenu |         return mensaMenu | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     @RequestMapping("/timetable") |     @RequestMapping("/timetable") | ||||||
|     fun timetable( |     fun timetable( | ||||||
|         @RequestParam(value = "courseName", defaultValue = "AI4") courseName: String, |         @RequestParam(value = "course", defaultValue = "AI4") courseName: String, | ||||||
|         @RequestParam(value = "week", defaultValue = "0") week: Int |         @RequestParam(value = "week", defaultValue = "0") week: Int | ||||||
|     ): TimetableCourseWeek { |     ): TimetableCourseWeek { | ||||||
|         logger.info("timetable request at ${LocalDateTime.now()}!") |         logger.info("timetable request at ${LocalDateTime.now()}!") | ||||||
|         updateTimetableRequests(courseName) |         updateTimetableRequests(courseName) | ||||||
|         totalRequests++ |         return getTimetable(courseName, week) | ||||||
|         return cache.getTimetable(courseName, week) |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     @RequestMapping("/lessonSubjectList") |     @RequestMapping("/subjectList") | ||||||
|     fun lessonSubjectList( |     fun lessonSubjectList( | ||||||
|         @RequestParam(value = "courseName", defaultValue = "AI4") courseName: String, |         @RequestParam(value = "course", defaultValue = "AI4") courseName: String, | ||||||
|         @RequestParam(value = "week", defaultValue = "0") week: Int |         @RequestParam(value = "week", defaultValue = "0") week: Int | ||||||
|     ): HashSet<String> { |     ): HashSet<String> { | ||||||
|         logger.info("lessonSubjectList request at ${LocalDateTime.now()}!") |         logger.info("subjectList request at ${LocalDateTime.now()}!") | ||||||
|         totalRequests++ |         updateTimetableRequests(courseName) | ||||||
|         return getLessonSubjectList(courseName, week) |         return getLessonSubjectList(courseName, week) | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     @RequestMapping("/lessons") |     @RequestMapping("/lessons") | ||||||
|     fun lesson( |     fun lesson( | ||||||
|         @RequestParam(value = "courseName", defaultValue = "AI4") courseName: String, |         @RequestParam(value = "course", defaultValue = "AI4") courseName: String, | ||||||
|         @RequestParam(value = "lessonSubject", defaultValue = "Mathematik 4") lessonSubject: String, |         @RequestParam(value = "subject", defaultValue = "Mathematik 4") lessonSubject: String, | ||||||
|         @RequestParam(value = "week", defaultValue = "0") week: Int |         @RequestParam(value = "week", defaultValue = "0") week: Int | ||||||
|     ): ArrayList<Lesson> { |     ): ArrayList<LessonWithRoom> { | ||||||
|         logger.info("lesson request at ${LocalDateTime.now()}!") |         logger.info("lesson request at ${LocalDateTime.now()}!") | ||||||
|         totalRequests++ |         updateTimetableRequests(courseName) | ||||||
|         return getLesson(courseName, lessonSubject, week) |         return getLesson(courseName, lessonSubject, week) | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     @RequestMapping("/roomList") | ||||||
|  |     fun roomList(): RoomsListRet { | ||||||
|  |         logger.info("roomList request at ${LocalDateTime.now()}!") | ||||||
|  |         updateRoomListRequests() | ||||||
|  |         return RoomsListRet(roomList.meta, ArrayList(roomList.rooms.values)) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @RequestMapping("/roomSchedule") | ||||||
|  |     fun roomSchedule( | ||||||
|  |         @RequestParam(value = "room", defaultValue = "B040") roomName: String, | ||||||
|  |         @RequestParam(value = "week", defaultValue = "0") week: Int | ||||||
|  |     ): RoomScheduleWeekRet { | ||||||
|  |         logger.info("roomSchedule request at ${LocalDateTime.now()}!") | ||||||
|  |         updateRoomScheduleRequests(roomName) | ||||||
|  |         return getRoomSchedule(roomName, week) | ||||||
|  |     } | ||||||
|  |  | ||||||
|     @RequestMapping("/status") |     @RequestMapping("/status") | ||||||
|     fun status(): Status { |     fun status(): Status { | ||||||
|  |         logger.info("status request at ${LocalDateTime.now()}!") | ||||||
|         return getStatus() |         return getStatus() | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     // non direct api functions |     @RequestMapping("/health") | ||||||
|  |     fun health(): Int { | ||||||
|     /** |         logger.info("health request at ${LocalDateTime.now()}!") | ||||||
|      * get every explicit lesson in a week |         return 200 | ||||||
|      * @param courseName the name of the course to be requested |  | ||||||
|      * @param weekIndex request week number (current week = 0) |  | ||||||
|      * @return a HashSet of explicit lessons for one week |  | ||||||
|      */ |  | ||||||
|     private fun getLessonSubjectList(courseName: String, weekIndex: Int): HashSet<String> { |  | ||||||
|         val lessonSubjectList = ArrayList<String>() |  | ||||||
|  |  | ||||||
|         // get every lesson subject for the given week |  | ||||||
|         val flatMap = cache.getTimetable(courseName, weekIndex).timetable.days.flatMap { it.timeslots.asIterable() } |  | ||||||
|         flatMap.forEach { |  | ||||||
|             it.stream().filter { x -> x.lessonSubject.isNotEmpty() }.findAny().ifPresent { x -> lessonSubjectList.add(x.lessonSubject) } |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         return HashSet(lessonSubjectList) |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * get every explicit lesson in a week |      * Deprecated section | ||||||
|      * @param courseName the name of the course to be requested |  | ||||||
|      * @param lessonSubject the lesson subject to be requested |  | ||||||
|      * @param weekIndex request week number (current week = 0) |  | ||||||
|      * @return a ArrayList of every lesson with lessonSubject for one week |  | ||||||
|      */ |      */ | ||||||
|     private fun getLesson(courseName: String, lessonSubject: String, weekIndex: Int): ArrayList<Lesson> { |  | ||||||
|         val lessonList = ArrayList<Lesson>() |  | ||||||
|  |  | ||||||
|         // get all lessons from the weeks timetable |     // TODO remove this with API version 2.0.0 | ||||||
|         val flatMap = cache.getTimetable(courseName, weekIndex).timetable.days.flatMap { it.timeslots.asIterable() } |     @Deprecated("courses is replaced by courseList", replaceWith = ReplaceWith("courseList()")) | ||||||
|         flatMap.forEach { |     @RequestMapping("/courses") | ||||||
|             it.forEach { lesson -> |     fun courses(): CoursesListRet { | ||||||
|                 if(lesson.lessonSubject.contains(lessonSubject)) { |         return courseList() | ||||||
|                     lessonList.add(lesson) |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
|  |  | ||||||
|             //it.stream().filter { x -> x.lessonSubject.contains(lessonSubject) }.findAny().ifPresent { x -> println("${x.lessonSubject}, ${x.lessonTeacher}") } |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         return lessonList |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     /** |     // TODO remove this with API version 2.0.0 | ||||||
|      * if a timetable is requested update the request counter |     @Deprecated("the parameter courseName is deprecated please use course", ReplaceWith("timetable(courseName, week)")) | ||||||
|      */ |     @RequestMapping("/timetable", params= ["courseName", "week"]) | ||||||
|     private fun updateTimetableRequests(courseName: String) { |     fun timetableDep( | ||||||
|         timetableRequests.stream().filter { it.courseName == courseName }.findFirst().ifPresentOrElse({ |         @RequestParam(value = "courseName", defaultValue = "AI4") courseName: String, | ||||||
|             it.requests++ |         @RequestParam(value = "week", defaultValue = "0") week: Int | ||||||
|         }, { |     ): TimetableCourseWeek { | ||||||
|             timetableRequests.add(TimetableCounter(courseName, 1)) |        return timetable(courseName, week) | ||||||
|         }) |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     private fun getStatus(): Status { |  | ||||||
|         val currentTime = System.currentTimeMillis() / 1000 |  | ||||||
|         val minutes = (currentTime - startTime) % 3600 / 60 |  | ||||||
|         val hours = (currentTime - startTime) % 86400 / 3600 |  | ||||||
|         val days = (currentTime - startTime) / 86400 |  | ||||||
|  |  | ||||||
|         var hsoCode = 999 |  | ||||||
|         var swfrCode = 999 |  | ||||||
|         logger.info("status request at ${LocalDateTime.now()}!") |  | ||||||
|  |  | ||||||
|         try { |  | ||||||
|             val hsoURL = URL("https://www.hs-offenburg.de/") |  | ||||||
|             val swfrURL = URL("https://www.swfr.de/") |  | ||||||
|  |  | ||||||
|             var connection = hsoURL.openConnection() as HttpURLConnection |  | ||||||
|             connection.requestMethod = "HEAD" |  | ||||||
|  |  | ||||||
|             connection.connectTimeout = 15000 |  | ||||||
|             hsoCode = connection.responseCode |  | ||||||
|  |  | ||||||
|             connection = swfrURL.openConnection() as HttpURLConnection |  | ||||||
|             connection.connectTimeout = 15000 |  | ||||||
|             swfrCode = connection.responseCode |  | ||||||
|         } catch (e: Exception) { |  | ||||||
|             logger.error("Error while fetching url response codes!", e) |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         return Status( |  | ||||||
|             LocalDateTime.now(), |  | ||||||
|             "$days days, $hours:$minutes", |  | ||||||
|             apiVersion, |  | ||||||
|             softwareVersion, |  | ||||||
|             totalRequests, |  | ||||||
|             mensaMenuRequests, |  | ||||||
|             timetableRequests, |  | ||||||
|             timetableList.size, |  | ||||||
|             Date(courseList.meta.updateTime * 1000), |  | ||||||
|             Date(mensaMenu.meta.updateTime * 1000), |  | ||||||
|             hsoCode, |  | ||||||
|             swfrCode |  | ||||||
|         ) |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
| } | } | ||||||
| @ -1,7 +1,7 @@ | |||||||
| /** | /** | ||||||
|  * TheCitadelofRicks |  * TheCitadelofRicks | ||||||
|  * |  * | ||||||
|  * Copyright 2019  <seil0@mosad.xyz> |  * Copyright 2019-2020 <seil0@mosad.xyz> | ||||||
|  * |  * | ||||||
|  * This program is free software; you can redistribute it and/or modify |  * This program is free software; you can redistribute it and/or modify | ||||||
|  * it under the terms of the GNU General Public License as published by |  * it under the terms of the GNU General Public License as published by | ||||||
|  | |||||||
| @ -1,190 +0,0 @@ | |||||||
| /** |  | ||||||
|  * TheCitadelofRicks |  | ||||||
|  * |  | ||||||
|  * Copyright 2019  <seil0@mosad.xyz> |  | ||||||
|  * |  | ||||||
|  * This program is free software; you can redistribute it and/or modify |  | ||||||
|  * it under the terms of the GNU General Public License as published by |  | ||||||
|  * the Free Software Foundation; either version 3 of the License, or |  | ||||||
|  * (at your option) any later version. |  | ||||||
|  * |  | ||||||
|  * This program is distributed in the hope that it will be useful, |  | ||||||
|  * but WITHOUT ANY WARRANTY; without even the implied warranty of |  | ||||||
|  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the |  | ||||||
|  * GNU General Public License for more details. |  | ||||||
|  * |  | ||||||
|  * You should have received a copy of the GNU General Public License |  | ||||||
|  * along with this program; if not, write to the Free Software |  | ||||||
|  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, |  | ||||||
|  * MA 02110-1301, USA. |  | ||||||
|  * |  | ||||||
|  */ |  | ||||||
|  |  | ||||||
| package org.mosad.thecitadelofricks |  | ||||||
|  |  | ||||||
| import kotlinx.coroutines.GlobalScope |  | ||||||
| import kotlinx.coroutines.async |  | ||||||
| import kotlinx.coroutines.launch |  | ||||||
| import kotlinx.coroutines.runBlocking |  | ||||||
| import org.mosad.thecitadelofricks.hsoparser.CourseListParser |  | ||||||
| import org.mosad.thecitadelofricks.hsoparser.MensaParser |  | ||||||
| import org.mosad.thecitadelofricks.hsoparser.TimetableParser |  | ||||||
| import org.slf4j.Logger |  | ||||||
| import org.slf4j.LoggerFactory |  | ||||||
| import java.util.* |  | ||||||
| import kotlin.collections.ArrayList |  | ||||||
| import kotlin.concurrent.scheduleAtFixedRate |  | ||||||
|  |  | ||||||
| class CacheController { |  | ||||||
|  |  | ||||||
|     private val logger: Logger = LoggerFactory.getLogger(CacheController::class.java) |  | ||||||
|  |  | ||||||
|     // hso parser links (hardcoded) |  | ||||||
|     private val courseListURL = "https://www.hs-offenburg.de/studium/vorlesungsplaene/" |  | ||||||
|     private val mensaMenuURL = "https://www.swfr.de/de/essen-trinken/speiseplaene/mensa-offenburg/" |  | ||||||
|     private val mensaName = "Offenburg" |  | ||||||
|  |  | ||||||
|     // cache objects |  | ||||||
|     companion object{ |  | ||||||
|  |  | ||||||
|         lateinit var courseList: CourseList |  | ||||||
|         lateinit var mensaMenu: MensaMenu |  | ||||||
|  |  | ||||||
|         var timetableList = ArrayList<TimetableCourseWeek>() // this list contains all timetables |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     init { |  | ||||||
|         initUpdates() |  | ||||||
|         scheduledUpdates() |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     /** |  | ||||||
|      * get a timetable, since they may not cached, we need to make sure it's cached, otherwise download |  | ||||||
|      * @param courseName the name of the course to be requested |  | ||||||
|      * @param weekIndex request week number (current week = 0) |  | ||||||
|      * @return the timetable of a course (courseName) |  | ||||||
|      */ |  | ||||||
|     fun getTimetable(courseName: String, weekIndex: Int): TimetableCourseWeek = runBlocking { |  | ||||||
|         val currentTime = System.currentTimeMillis() / 1000 |  | ||||||
|         var timetable = TimetableWeek() |  | ||||||
|         var weekNumberYear = 0 |  | ||||||
|  |  | ||||||
|         // check if the timetable already exists and is up to date |  | ||||||
|         when (timetableList.stream().filter { x -> x.meta.courseName == courseName && x.meta.weekIndex == weekIndex }.findAny().orElse(null)) { |  | ||||||
|             // there is no such course yet, create one |  | ||||||
|             null -> { |  | ||||||
|                 val courseLink = courseList.courses.stream().filter { x -> x.courseName == courseName }.findFirst().orElse(null).courseLink |  | ||||||
|                 val timetableLink = courseLink.replace("week=0","week=$weekIndex") |  | ||||||
|  |  | ||||||
|                 val jobTimetable = GlobalScope.async { |  | ||||||
|                     timetable = TimetableParser().getTimeTable(timetableLink) |  | ||||||
|                     weekNumberYear = TimetableParser().getWeekNumberYear(timetableLink) |  | ||||||
|                 } |  | ||||||
|  |  | ||||||
|                 jobTimetable.await() |  | ||||||
|  |  | ||||||
|                 timetableList.add(TimetableCourseWeek(TimetableCourseMeta(currentTime, courseName, weekIndex, weekNumberYear, timetableLink), timetable)) |  | ||||||
|                 logger.info("added new timetable for $courseName, week $weekIndex") |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         return@runBlocking timetableList.stream().filter { x -> x.meta.courseName == courseName && x.meta.weekIndex == weekIndex }.findAny().orElse(null) |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     /** |  | ||||||
|      * this function updates the courseList |  | ||||||
|      * during the update process the old data will be returned for a API request |  | ||||||
|      */ |  | ||||||
|     private fun asyncUpdateCourseList() = GlobalScope.launch { |  | ||||||
|         CourseListParser().getCourseLinks(courseListURL)?.let { |  | ||||||
|             courseList = CourseList(CourseMeta(System.currentTimeMillis() / 1000, it.size), it) |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         logger.info("updated courses successful at ${Date(courseList.meta.updateTime * 1000)}") |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     /** |  | ||||||
|      * this function updates the mensa menu list |  | ||||||
|      * during the update process the old data will be returned for a API request |  | ||||||
|      */ |  | ||||||
|     private fun asyncUpdateMensa() = GlobalScope.launch { |  | ||||||
|         val mensaCurrentWeek = MensaParser().getMensaMenu(mensaMenuURL) |  | ||||||
|         val mensaNextWeek = MensaParser().getMensaMenu(MensaParser().getMenuLinkNextWeek(mensaMenuURL)) |  | ||||||
|  |  | ||||||
|         // only update if we get valid data |  | ||||||
|         if (mensaCurrentWeek != null && mensaNextWeek != null) { |  | ||||||
|             mensaMenu = MensaMenu(MensaMeta(System.currentTimeMillis() / 1000, mensaName), mensaCurrentWeek, mensaNextWeek) |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         logger.info("updated mensamenu successful at ${Date(mensaMenu.meta.updateTime * 1000)}") |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     /** |  | ||||||
|      * this function updates all existing timetables |  | ||||||
|      * during the update process the old data will be returned for a API request |  | ||||||
|      */ |  | ||||||
|     private fun asyncUpdateTimetables() = GlobalScope.launch { |  | ||||||
|         timetableList.forEach { timetableCourse -> |  | ||||||
|             val updateURL = timetableCourse.meta.link |  | ||||||
|             timetableCourse.timetable = TimetableParser().getTimeTable(updateURL) |  | ||||||
|             timetableCourse.meta.updateTime = System.currentTimeMillis() / 1000 |  | ||||||
|         } |  | ||||||
|         logger.info("updated ${timetableList.size} timetables successful!") |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     /** |  | ||||||
|      * before the APIController is up, get the data fist |  | ||||||
|      * runBlocking: otherwise the api would return no data to requests for a few seconds after startup |  | ||||||
|      */ |  | ||||||
|     private fun initUpdates() = runBlocking { |  | ||||||
|         // get all courses on startup |  | ||||||
|         val jobCourseUpdate = GlobalScope.async { |  | ||||||
|             CourseListParser().getCourseLinks(courseListURL)?.let { |  | ||||||
|                 courseList = CourseList(CourseMeta(System.currentTimeMillis() / 1000, it.size), it) |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         // get the current and next weeks mensa menus |  | ||||||
|         val jobMensa = GlobalScope.async{ |  | ||||||
|             val mensaCurrentWeek = MensaParser().getMensaMenu(mensaMenuURL) |  | ||||||
|             val mensaNextWeek = MensaParser().getMensaMenu(MensaParser().getMenuLinkNextWeek(mensaMenuURL)) |  | ||||||
|  |  | ||||||
|             // only update if we get valid data |  | ||||||
|             if (mensaCurrentWeek != null && mensaNextWeek != null) { |  | ||||||
|                 mensaMenu = MensaMenu(MensaMeta(System.currentTimeMillis() / 1000, mensaName), mensaCurrentWeek, mensaNextWeek) |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         jobCourseUpdate.await() |  | ||||||
|         jobMensa.await() |  | ||||||
|  |  | ||||||
|         logger.info("init updates successful") |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     /** |  | ||||||
|      * doesn't account the change between winter and summer time! |  | ||||||
|      * |  | ||||||
|      */ |  | ||||||
|     private fun scheduledUpdates() { |  | ||||||
|         val currentTime = System.currentTimeMillis() |  | ||||||
|         val initDelay24h = (86400000 - ((currentTime + 3600000) % 86400000)) + 60000 |  | ||||||
|         val initDelay3h = (10800000 - ((currentTime + 3600000) % 10800000)) + 60000 |  | ||||||
|         val initDelay1h = (3600000 - ((currentTime + 3600000) % 3600000)) + 60000 |  | ||||||
|  |  | ||||||
|         // update courseList every 24 hours (time in ms) |  | ||||||
|         Timer().scheduleAtFixedRate(initDelay24h, 86400000) { |  | ||||||
|             asyncUpdateCourseList() |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         // update all already existing timetables every 3 hours (time in ms) |  | ||||||
|         Timer().scheduleAtFixedRate(initDelay3h, 10800000) { |  | ||||||
|             asyncUpdateTimetables() |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         // update courses every hour (time in ms) |  | ||||||
|         Timer().scheduleAtFixedRate(initDelay1h, 3600000) { |  | ||||||
|             asyncUpdateMensa() |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
| } |  | ||||||
| @ -1,7 +1,7 @@ | |||||||
| /** | /** | ||||||
|  * TheCitadelofRicks |  * TheCitadelofRicks | ||||||
|  * |  * | ||||||
|  * Copyright 2019  <seil0@mosad.xyz> |  * Copyright 2019-2020  <seil0@mosad.xyz> | ||||||
|  * |  * | ||||||
|  * This program is free software; you can redistribute it and/or modify |  * This program is free software; you can redistribute it and/or modify | ||||||
|  * it under the terms of the GNU General Public License as published by |  * it under the terms of the GNU General Public License as published by | ||||||
| @ -24,13 +24,15 @@ package org.mosad.thecitadelofricks | |||||||
|  |  | ||||||
| import java.time.LocalDateTime | import java.time.LocalDateTime | ||||||
| import java.util.* | import java.util.* | ||||||
|  | import kotlin.collections.HashMap | ||||||
|  |  | ||||||
| // data classes for the course part | // data classes for the course part | ||||||
| data class Course(val courseName: String, val courseLink: String) | data class Course(val courseName: String, val courseLink: String) | ||||||
|  |  | ||||||
| data class CourseMeta(val updateTime: Long, val totalCourses: Int) | data class CoursesMeta(val updateTime: Long = 0, val totalCourses: Int = 0) | ||||||
|  |  | ||||||
| data class CourseList(val meta: CourseMeta, val courses: ArrayList<Course>) | data class CoursesList(val meta: CoursesMeta = CoursesMeta(), val courses: SortedMap<String, Course>) | ||||||
|  | data class CoursesListRet(val meta: CoursesMeta = CoursesMeta(), val courses: ArrayList<Course> = ArrayList()) | ||||||
|  |  | ||||||
| // data classes for the Mensa part | // data classes for the Mensa part | ||||||
| data class Meal(val day: String, val heading: String, val parts: ArrayList<String>, val additives: String) | data class Meal(val day: String, val heading: String, val parts: ArrayList<String>, val additives: String) | ||||||
| @ -44,7 +46,10 @@ data class MensaMeta(val updateTime: Long, val mensaName: String) | |||||||
| data class MensaMenu(val meta: MensaMeta, val currentWeek: MensaWeek, val nextWeek: MensaWeek) | data class MensaMenu(val meta: MensaMeta, val currentWeek: MensaWeek, val nextWeek: MensaWeek) | ||||||
|  |  | ||||||
| // data classes for the timetable part | // data classes for the timetable part | ||||||
| data class Lesson( |  | ||||||
|  | data class CalendarWeek(val week: Int, val year: Int) | ||||||
|  |  | ||||||
|  | data class LessonWithRoom( | ||||||
|     val lessonID: String, |     val lessonID: String, | ||||||
|     val lessonSubject: String, |     val lessonSubject: String, | ||||||
|     val lessonTeacher: String, |     val lessonTeacher: String, | ||||||
| @ -52,28 +57,52 @@ data class Lesson( | |||||||
|     val lessonRemark: String |     val lessonRemark: String | ||||||
| ) | ) | ||||||
|  |  | ||||||
| data class TimetableDay(val timeslots: Array<ArrayList<Lesson>> = Array(6) { ArrayList<Lesson>() }) | data class LessonWithCourse( | ||||||
|  |     val lessonID: String, | ||||||
|  |     val lessonSubject: String, | ||||||
|  |     val lessonTeacher: String, | ||||||
|  |     val lessonCourse: String, | ||||||
|  |     val lessonRemark: String | ||||||
|  | ) | ||||||
|  |  | ||||||
| data class TimetableWeek(val days: Array<TimetableDay> = Array(6) { TimetableDay() }) | data class TimetableDay<Lesson>(val timeslots: Array<ArrayList<Lesson>> = Array(6) { ArrayList<Lesson>() }) | ||||||
|  |  | ||||||
| data class TimetableCourseMeta(var updateTime: Long, val courseName: String, val weekIndex: Int, val weekNumberYear: Int, val link: String) | data class TimetableWeek<Lesson>(val days: Array<TimetableDay<Lesson>> = Array(6) { TimetableDay() }) | ||||||
|  |  | ||||||
| data class TimetableCourseWeek(val meta: TimetableCourseMeta, var timetable: TimetableWeek) | data class TimetableCourseMeta(var updateTime: Long = 0, val courseName: String = "", val weekIndex: Int = 0, var weekNumberYear: Int = 0, var year: Int = 0, val link: String = "") | ||||||
|  |  | ||||||
|  | data class TimetableCourseWeek(val meta: TimetableCourseMeta = TimetableCourseMeta(), var timetable: TimetableWeek<LessonWithRoom> = TimetableWeek()) | ||||||
|  |  | ||||||
|  | // data classes for the room occupancy part | ||||||
|  | data class Room(val roomName: String, val roomLink: String) | ||||||
|  |  | ||||||
|  | data class RoomsMeta(val updateTime: Long = 0, val totalRooms: Int = 0) | ||||||
|  |  | ||||||
|  | data class RoomsList(val meta: RoomsMeta = RoomsMeta(), val rooms: SortedMap<String, Room>) | ||||||
|  | data class RoomsListRet(val meta: RoomsMeta = RoomsMeta(), val rooms: ArrayList<Room> = ArrayList()) | ||||||
|  |  | ||||||
|  | data class RoomScheduleMeta(var updateTime: Long = 0, val roomName: String = "", val weekIndex: Int = 0, var weekNumberYear: Int = 0, var year: Int = 0, val link: String = "") | ||||||
|  |  | ||||||
|  | data class RoomScheduleWeekRet(val meta: RoomScheduleMeta = RoomScheduleMeta(), var timetable: TimetableWeek<LessonWithCourse> = TimetableWeek()) | ||||||
|  |  | ||||||
|  |  | ||||||
| // data classes for the status part | // data classes for the status part | ||||||
| data class TimetableCounter(var courseName: String, var requests: Int) |  | ||||||
|  |  | ||||||
| data class Status( | data class Status( | ||||||
|     val time: LocalDateTime, |     val time: LocalDateTime, | ||||||
|     val uptime: String, |     val uptime: String, | ||||||
|     val apiVersion: String, |     val apiVersion: String, | ||||||
|     val softwareVersion: String, |     val softwareVersion: String, | ||||||
|     val requestCount: Int, |     val totalRequests: Int, | ||||||
|     val mensaMenuRequests: Int, |     val mensaMenuRequests: Int, | ||||||
|     val timetableRequests: ArrayList<TimetableCounter>, |     val courseListRequests: Int, | ||||||
|  |     val timetableRequests: HashMap<String, Int>, | ||||||
|     val timetableListSize: Int, |     val timetableListSize: Int, | ||||||
|     val coursesLastUpdate: Date, |     val coursesLastUpdate: Date, | ||||||
|  |     val roomListRequests: Int, | ||||||
|  |     val roomScheduleRequests: HashMap<String, Int>, | ||||||
|  |     val roomScheduleListSize: Int, | ||||||
|  |     val roomsLastUpdate: Date, | ||||||
|     val mensaLastUpdate: Date, |     val mensaLastUpdate: Date, | ||||||
|     val hsoResponseCode: Int, |     val hsoResponseCode: Int, | ||||||
|     val swfrResponseCode: Int |     val swfrResponseCode: Int | ||||||
|  | |||||||
| @ -0,0 +1,399 @@ | |||||||
|  | /** | ||||||
|  |  * TheCitadelofRicks | ||||||
|  |  * | ||||||
|  |  * Copyright 2019-2020  <seil0@mosad.xyz> | ||||||
|  |  * | ||||||
|  |  * This program is free software; you can redistribute it and/or modify | ||||||
|  |  * it under the terms of the GNU General Public License as published by | ||||||
|  |  * the Free Software Foundation; either version 3 of the License, or | ||||||
|  |  * (at your option) any later version. | ||||||
|  |  * | ||||||
|  |  * This program is distributed in the hope that it will be useful, | ||||||
|  |  * but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||||
|  |  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||||
|  |  * GNU General Public License for more details. | ||||||
|  |  * | ||||||
|  |  * You should have received a copy of the GNU General Public License | ||||||
|  |  * along with this program; if not, write to the Free Software | ||||||
|  |  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, | ||||||
|  |  * MA 02110-1301, USA. | ||||||
|  |  * | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | package org.mosad.thecitadelofricks.controller | ||||||
|  |  | ||||||
|  | import com.google.gson.Gson | ||||||
|  | import kotlinx.coroutines.* | ||||||
|  | import org.jsoup.Jsoup | ||||||
|  | import org.mosad.thecitadelofricks.* | ||||||
|  | import org.mosad.thecitadelofricks.hsoparser.CourseListParser | ||||||
|  | import org.mosad.thecitadelofricks.hsoparser.CourseTimetableParser | ||||||
|  | import org.mosad.thecitadelofricks.hsoparser.MensaParser | ||||||
|  | import org.mosad.thecitadelofricks.hsoparser.RoomListParser | ||||||
|  | import org.mosad.thecitadelofricks.hsoparser.RoomTimetableParser | ||||||
|  | import org.slf4j.Logger | ||||||
|  | import org.slf4j.LoggerFactory | ||||||
|  | import java.io.* | ||||||
|  | import java.util.* | ||||||
|  | import java.util.concurrent.ConcurrentHashMap | ||||||
|  | import java.util.concurrent.Executors | ||||||
|  | import kotlin.collections.ArrayList | ||||||
|  | import kotlin.collections.HashSet | ||||||
|  | import kotlin.concurrent.scheduleAtFixedRate | ||||||
|  | import kotlin.time.Duration.Companion.hours | ||||||
|  | import kotlin.time.Duration.Companion.minutes | ||||||
|  |  | ||||||
|  | class CacheController { | ||||||
|  |  | ||||||
|  |     init { | ||||||
|  |         initUpdates() | ||||||
|  |         scheduledUpdates() | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     companion object { | ||||||
|  |         private val logger: Logger = LoggerFactory.getLogger(CacheController::class.java) | ||||||
|  |  | ||||||
|  |         var courseList = CoursesList(CoursesMeta(), sortedMapOf()) | ||||||
|  |         var mensaMenu = MensaMenu(MensaMeta(0, ""), MensaWeek(), MensaWeek()) | ||||||
|  |         var timetableList = ConcurrentHashMap<String, TimetableCourseWeek>() // this list contains all timetables | ||||||
|  |         var roomList = RoomsList(RoomsMeta(), sortedMapOf()) | ||||||
|  |         var roomScheduleList = ConcurrentHashMap<String, RoomScheduleWeekRet>() // this list contains all room schedules | ||||||
|  |  | ||||||
|  |         /** | ||||||
|  |          * get a timetable, since they may not be cached, we need to make sure it's cached, otherwise download | ||||||
|  |          * @param courseName the name of the course to be requested | ||||||
|  |          * @param weekIndex request week number (current week = 0) | ||||||
|  |          * @return timetable of the course (Type: [TimetableCourseWeek]) | ||||||
|  |          */ | ||||||
|  |         fun getTimetable(courseName: String, weekIndex: Int): TimetableCourseWeek { | ||||||
|  |  | ||||||
|  |             // TODO just for testing | ||||||
|  |             if (courseName == "TEST_A" || courseName == "TEST_B") { | ||||||
|  |                 val currentTime = System.currentTimeMillis() / 1000 | ||||||
|  |                 val timetableLink = "https://mosad.xyz" | ||||||
|  |                 val weekNumberYear = 0 | ||||||
|  |                 val year = 0 | ||||||
|  |                 val instr = CacheController::class.java.getResourceAsStream("/html/Timetable_normal-week.html") | ||||||
|  |  | ||||||
|  |                 val timetableParser = | ||||||
|  |                     CourseTimetableParser(htmlDoc = Jsoup.parse(instr!!, "UTF-8", "https://www.hs-offenburg.de/")) | ||||||
|  |                 val timetableTest = timetableParser.parseTimeTable() | ||||||
|  |  | ||||||
|  |                 return TimetableCourseWeek( | ||||||
|  |                     TimetableCourseMeta( | ||||||
|  |                         currentTime, | ||||||
|  |                         courseName, | ||||||
|  |                         weekIndex, | ||||||
|  |                         weekNumberYear, | ||||||
|  |                         year, | ||||||
|  |                         timetableLink | ||||||
|  |                     ), timetableTest ?: TimetableWeek() | ||||||
|  |                 ) | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             val key = "$courseName-$weekIndex" | ||||||
|  |             return if (timetableList.containsKey(key)) { | ||||||
|  |                 timetableList[key]!! | ||||||
|  |             } else { | ||||||
|  |                 val timetableLink = courseList.courses[courseName] | ||||||
|  |                     ?.courseLink | ||||||
|  |                     ?.replace("week=0", "week=$weekIndex") ?: "" | ||||||
|  |                 val currentTime = System.currentTimeMillis() / 1000 | ||||||
|  |  | ||||||
|  |                 val timetableParser = CourseTimetableParser(timetableLink) | ||||||
|  |                 val calendarWeek = timetableParser.parseCalendarWeek() | ||||||
|  |                 val timetable = timetableParser.parseTimeTable() | ||||||
|  |  | ||||||
|  |                 TimetableCourseWeek( | ||||||
|  |                     TimetableCourseMeta( | ||||||
|  |                         currentTime, | ||||||
|  |                         courseName, | ||||||
|  |                         weekIndex, | ||||||
|  |                         calendarWeek?.week ?: 0, | ||||||
|  |                         calendarWeek?.year ?: 0, | ||||||
|  |                         timetableLink | ||||||
|  |                     ), timetable ?: TimetableWeek() | ||||||
|  |                 ).also { if (timetable != null) timetableList[key] = it } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         /** | ||||||
|  |          * get every explicit lesson in a week for a selected course | ||||||
|  |          * @param courseName the name of the course to be requested | ||||||
|  |          * @param weekIndex request week number (current week = 0) | ||||||
|  |          * @return a HashSet of explicit lessons for one week | ||||||
|  |          */ | ||||||
|  |         fun getLessonSubjectList(courseName: String, weekIndex: Int): HashSet<String> = runBlocking { | ||||||
|  |             val lessonSubjectList = ArrayList<String>() | ||||||
|  |  | ||||||
|  |             // get every lesson subject for the given week | ||||||
|  |             val flatMap = getTimetable(courseName, weekIndex).timetable.days.flatMap { it.timeslots.asIterable() } | ||||||
|  |             flatMap.forEach { | ||||||
|  |                 it.stream().filter { x -> x.lessonSubject.isNotEmpty() }.findAny().ifPresent { x -> lessonSubjectList.add(x.lessonSubject) } | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             return@runBlocking HashSet(lessonSubjectList) | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         /** | ||||||
|  |          * get every lesson of a subject in a week | ||||||
|  |          * @param courseName the name of the course to be requested | ||||||
|  |          * @param lessonSubject the lesson subject to be requested | ||||||
|  |          * @param weekIndex request week number (current week = 0) | ||||||
|  |          * @return a ArrayList<[Lesson]> of every lesson with lessonSubject for one week | ||||||
|  |          */ | ||||||
|  |         fun getLesson(courseName: String, lessonSubject: String, weekIndex: Int): ArrayList<LessonWithRoom> { | ||||||
|  |             val lessonList = ArrayList<LessonWithRoom>() | ||||||
|  |  | ||||||
|  |             // get all lessons from the weeks timetable | ||||||
|  |             val flatMap = getTimetable(courseName, weekIndex).timetable.days.flatMap { it.timeslots.asIterable() } | ||||||
|  |             flatMap.forEach { | ||||||
|  |                 it.stream().filter { x -> x.lessonSubject.contains(lessonSubject) }.findAny().ifPresent { x -> lessonList.add(x) } | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             return lessonList | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         /** | ||||||
|  |          * Get a room schedule. | ||||||
|  |          * Since they may not be cached, we need to make sure it's cached. Download the schedule if it is not cached. | ||||||
|  |          * @param roomName the name of the room to be requested | ||||||
|  |          * @param weekIndex request week number (current week = 0) | ||||||
|  |          * @return room schedule of the room (Type: [RoomScheduleWeekRet]) | ||||||
|  |          */ | ||||||
|  |         fun getRoomSchedule(roomName: String, weekIndex: Int): RoomScheduleWeekRet { | ||||||
|  |             val key = "$roomName-$weekIndex" | ||||||
|  |             return if (roomScheduleList.containsKey(key)) { | ||||||
|  |                 roomScheduleList[key]!! | ||||||
|  |             } else { | ||||||
|  |                 val roomScheduleLink = roomList.rooms[roomName] | ||||||
|  |                     ?.roomLink | ||||||
|  |                     ?.replace("week=0", "week=$weekIndex") ?: "" | ||||||
|  |                 val currentTime = System.currentTimeMillis() / 1000 | ||||||
|  |  | ||||||
|  |                 val roomScheduleParser = RoomTimetableParser(roomScheduleLink) | ||||||
|  |                 val calendarWeek = roomScheduleParser.parseCalendarWeek() | ||||||
|  |                 val roomSchedule = roomScheduleParser.parseTimeTable() | ||||||
|  |  | ||||||
|  |                 RoomScheduleWeekRet( | ||||||
|  |                     RoomScheduleMeta( | ||||||
|  |                         currentTime, | ||||||
|  |                         roomName, | ||||||
|  |                         weekIndex, | ||||||
|  |                         calendarWeek?.week ?: 0, | ||||||
|  |                         calendarWeek?.year ?: 0, | ||||||
|  |                         roomScheduleLink | ||||||
|  |                     ), roomSchedule ?: TimetableWeek() | ||||||
|  |                 ).also { if (roomSchedule != null) roomScheduleList[key] = it } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         // private cache functions | ||||||
|  |  | ||||||
|  |         /** | ||||||
|  |          * this function updates the courseList | ||||||
|  |          * during the update process the old data will be returned for an API request | ||||||
|  |          */ | ||||||
|  |         private fun asyncUpdateCourseList() = CoroutineScope(Dispatchers.IO).launch { | ||||||
|  |             CourseListParser().getLinks(StartupController.courseListURL)?.let { | ||||||
|  |                 courseList = CoursesList(CoursesMeta(System.currentTimeMillis() / 1000, it.size), it.toSortedMap()) | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             // TODO just for testing | ||||||
|  |             courseList.courses["TEST_A"] = Course("TEST_A", "https://mosad.xyz") | ||||||
|  |             courseList.courses["TEST_B"] = Course("TEST_B", "https://mosad.xyz") | ||||||
|  |  | ||||||
|  |             logger.info("Updated courses successfully at ${Date(courseList.meta.updateTime * 1000)}") | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         /** | ||||||
|  |          * this function updates the roomList | ||||||
|  |          * during the update process the old data will be returned for an API request | ||||||
|  |          */ | ||||||
|  |         private fun asyncUpdateRoomList() = CoroutineScope(Dispatchers.IO).launch { | ||||||
|  |             RoomListParser().getLinks(StartupController.roomListURL)?.let { | ||||||
|  |                 roomList = RoomsList(RoomsMeta(System.currentTimeMillis() / 1000, it.size), it.toSortedMap()) | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             logger.info("Updated room list successfully at ${Date(courseList.meta.updateTime * 1000)}") | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         /** | ||||||
|  |          * this function updates the mensa menu list | ||||||
|  |          * during the update process the old data will be returned for an API request | ||||||
|  |          */ | ||||||
|  |         private fun asyncUpdateMensa() = CoroutineScope(Dispatchers.IO).launch { | ||||||
|  |             val mensaCurrentWeek = MensaParser().getMensaMenu(StartupController.mensaMenuURL) | ||||||
|  |             val mensaNextWeek = MensaParser().getMensaMenu(MensaParser().getMenuLinkNextWeek(StartupController.mensaMenuURL)) | ||||||
|  |  | ||||||
|  |             // only update if we get valid data | ||||||
|  |             if (mensaCurrentWeek != null && mensaNextWeek != null) { | ||||||
|  |                 mensaMenu = MensaMenu(MensaMeta(System.currentTimeMillis() / 1000, StartupController.mensaName), mensaCurrentWeek, mensaNextWeek) | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             logger.info("Updated mensa menu successfully at ${Date(mensaMenu.meta.updateTime * 1000)}") | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         /** | ||||||
|  |          * this function updates all existing timetables | ||||||
|  |          * during the update process the old data will be returned for an API request | ||||||
|  |          * a FixedThreadPool is used to make parallel requests for faster updates | ||||||
|  |          */ | ||||||
|  |         private fun asyncUpdateTimetables() = CoroutineScope(Dispatchers.IO).launch { | ||||||
|  |             logger.info("Updating ${timetableList.size} timetables ...") | ||||||
|  |  | ||||||
|  |             // create a new ThreadPool with 5 threads | ||||||
|  |             val executor = Executors.newFixedThreadPool(5) | ||||||
|  |  | ||||||
|  |             try { | ||||||
|  |                 timetableList.forEach { timetableCourse -> | ||||||
|  |                     executor.execute { | ||||||
|  |                         val timetableParser = CourseTimetableParser(timetableCourse.value.meta.link) | ||||||
|  |                         timetableCourse.value.timetable = timetableParser.parseTimeTable() ?: return@execute | ||||||
|  |                         timetableParser.parseCalendarWeek()?.also { | ||||||
|  |                             timetableCourse.value.meta.weekNumberYear = it.week | ||||||
|  |                             timetableCourse.value.meta.year = it.year | ||||||
|  |                         } ?: return@execute | ||||||
|  |                         timetableCourse.value.meta.updateTime = System.currentTimeMillis() / 1000 | ||||||
|  |  | ||||||
|  |                         saveTimetableToCache(timetableCourse.value) // save the updated timetable to the cache directory | ||||||
|  |                     } | ||||||
|  |  | ||||||
|  |                 } | ||||||
|  |             } catch (ex: Exception) { | ||||||
|  |                 logger.error("Error while updating the timetables", ex) | ||||||
|  |             } finally { | ||||||
|  |                 executor.shutdown() | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         /** | ||||||
|  |          * save a timetable to the cache directory | ||||||
|  |          * this is only call on async updates, it is NOT call when first getting the timetable | ||||||
|  |          * @param timetable a timetable of the type [TimetableCourseWeek] | ||||||
|  |          */ | ||||||
|  |         private fun saveTimetableToCache(timetable: TimetableCourseWeek) { | ||||||
|  |             val file = File(StartupController.dirTcorCache, "timetable-${timetable.meta.courseName}-${timetable.meta.weekIndex}.json") | ||||||
|  |             val writer = BufferedWriter(FileWriter(file)) | ||||||
|  |  | ||||||
|  |             try { | ||||||
|  |                 writer.write(Gson().toJson(timetable)) | ||||||
|  |             } catch (e: Exception) { | ||||||
|  |                 logger.error("something went wrong while trying to write a cache file", e) | ||||||
|  |             } finally { | ||||||
|  |                 writer.close() | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         private fun asyncUpdateRoomSchedules() = CoroutineScope(Dispatchers.IO).launch { | ||||||
|  |             logger.info("Updating ${roomScheduleList.size} room schedules ...") | ||||||
|  |  | ||||||
|  |             // create a new ThreadPool with 5 threads | ||||||
|  |             val executor = Executors.newFixedThreadPool(5) | ||||||
|  |  | ||||||
|  |             try { | ||||||
|  |                 roomScheduleList.forEach { roomSchedule -> | ||||||
|  |                     executor.execute { | ||||||
|  |                         val roomScheduleParser = RoomTimetableParser(roomSchedule.value.meta.link) | ||||||
|  |                         roomSchedule.value.timetable = roomScheduleParser.parseTimeTable() ?: return@execute | ||||||
|  |                         roomScheduleParser.parseCalendarWeek()?.also { | ||||||
|  |                             roomSchedule.value.meta.weekNumberYear = it.week | ||||||
|  |                             roomSchedule.value.meta.year = it.year | ||||||
|  |                         } ?: return@execute | ||||||
|  |                         roomSchedule.value.meta.updateTime = System.currentTimeMillis() / 1000 | ||||||
|  |  | ||||||
|  |                         saveRoomScheduleToCache(roomSchedule.value) // save the updated timetable to the cache directory | ||||||
|  |                     } | ||||||
|  |  | ||||||
|  |                 } | ||||||
|  |             } catch (ex: Exception) { | ||||||
|  |                 logger.error("Error while updating the room schedules", ex) | ||||||
|  |             } finally { | ||||||
|  |                 executor.shutdown() | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         /** | ||||||
|  |          * save a timetable to the cache directory | ||||||
|  |          * this is only call on async updates, it is NOT call when first getting the timetable | ||||||
|  |          * @param roomSchedule a room schedule of the type [RoomScheduleWeekRet] | ||||||
|  |          */ | ||||||
|  |         private fun saveRoomScheduleToCache(roomSchedule: RoomScheduleWeekRet) { | ||||||
|  |             val file = File(StartupController.dirTcorCache, "roomSchedule-${roomSchedule.meta.roomName}-${roomSchedule.meta.weekIndex}.json") | ||||||
|  |             val writer = BufferedWriter(FileWriter(file)) | ||||||
|  |  | ||||||
|  |             try { | ||||||
|  |                 writer.write(Gson().toJson(roomSchedule)) | ||||||
|  |             } catch (e: Exception) { | ||||||
|  |                 logger.error("something went wrong while trying to write a cache file", e) | ||||||
|  |             } finally { | ||||||
|  |                 writer.close() | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         /** | ||||||
|  |          * before the APIController is up, get the data fist | ||||||
|  |          * runBlocking: otherwise the api would return no data to requests for a few seconds after startup | ||||||
|  |          */ | ||||||
|  |         private fun initUpdates() = runBlocking { | ||||||
|  |             // get all course links on startup, make sure there are course links | ||||||
|  |             val jobCourseUpdate = asyncUpdateCourseList() | ||||||
|  |             val jobMensa = asyncUpdateMensa() | ||||||
|  |             val jobRoomListUpdate = asyncUpdateRoomList() | ||||||
|  |  | ||||||
|  |             jobCourseUpdate.join() | ||||||
|  |             jobMensa.join() | ||||||
|  |             jobRoomListUpdate.join() | ||||||
|  |  | ||||||
|  |             logger.info("Initial updates successful") | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         /** | ||||||
|  |          * update the CourseList every 24h, the Timetables every 3h and the Mensa Menu every hour | ||||||
|  |          * doesn't account the change between winter and summer time! | ||||||
|  |          */ | ||||||
|  |         private fun scheduledUpdates() { | ||||||
|  |             val currentTime = System.currentTimeMillis() | ||||||
|  |  | ||||||
|  |             val duration24h = 24.hours.inWholeMilliseconds | ||||||
|  |             val duration3h = 3.hours.inWholeMilliseconds | ||||||
|  |             val duration1h = 1.hours.inWholeMilliseconds | ||||||
|  |             val duration1m = 1.minutes.inWholeMilliseconds | ||||||
|  |  | ||||||
|  |             // Calculate the initial delay to make the update time independent of the start time | ||||||
|  |             fun calcInitDelay(period: Long) = (period - ((currentTime + duration1h) % period)) + duration1m | ||||||
|  |             val initDelay24h = calcInitDelay(duration24h) | ||||||
|  |             val initDelay3h = calcInitDelay(duration3h) | ||||||
|  |             val initDelay1h = calcInitDelay(duration1h) | ||||||
|  |  | ||||||
|  |             // update courseList and roomList every 24 hours (time in ms) | ||||||
|  |             Timer().scheduleAtFixedRate(initDelay24h, duration24h) { | ||||||
|  |                 asyncUpdateCourseList() | ||||||
|  |             } | ||||||
|  |             Timer().scheduleAtFixedRate(initDelay24h, duration24h) { | ||||||
|  |                 asyncUpdateRoomList() | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             // update all already existing timetables and room schedules every 3 hours (time in ms) | ||||||
|  |             Timer().scheduleAtFixedRate(initDelay3h, duration3h) { | ||||||
|  |                 asyncUpdateTimetables() | ||||||
|  |             } | ||||||
|  |             Timer().scheduleAtFixedRate(initDelay3h, duration3h) { | ||||||
|  |                 asyncUpdateRoomSchedules() | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             // update mensa menu every hour (time in ms) | ||||||
|  |             Timer().scheduleAtFixedRate(initDelay1h, duration1h) { | ||||||
|  |                 asyncUpdateMensa() | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             // post to status.mosad.xyz every hour, if an API key is present | ||||||
|  |             if (StartupController.cachetAPIKey != "0") { | ||||||
|  |                 Timer().scheduleAtFixedRate(initDelay1h, duration1h) { | ||||||
|  |                     CachetAPIController.postTotalRequests() | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |     } | ||||||
|  | } | ||||||
| @ -0,0 +1,75 @@ | |||||||
|  | /** | ||||||
|  |  * TheCitadelofRicks | ||||||
|  |  * | ||||||
|  |  * Copyright 2019-2020  <seil0@mosad.xyz> | ||||||
|  |  * | ||||||
|  |  * This program is free software; you can redistribute it and/or modify | ||||||
|  |  * it under the terms of the GNU General Public License as published by | ||||||
|  |  * the Free Software Foundation; either version 3 of the License, or | ||||||
|  |  * (at your option) any later version. | ||||||
|  |  * | ||||||
|  |  * This program is distributed in the hope that it will be useful, | ||||||
|  |  * but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||||
|  |  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||||
|  |  * GNU General Public License for more details. | ||||||
|  |  * | ||||||
|  |  * You should have received a copy of the GNU General Public License | ||||||
|  |  * along with this program; if not, write to the Free Software | ||||||
|  |  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, | ||||||
|  |  * MA 02110-1301, USA. | ||||||
|  |  * | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | package org.mosad.thecitadelofricks.controller | ||||||
|  |  | ||||||
|  | import org.mosad.thecitadelofricks.controller.StatusController.Companion.totalRequests | ||||||
|  | import org.slf4j.Logger | ||||||
|  | import org.slf4j.LoggerFactory | ||||||
|  | import java.io.BufferedReader | ||||||
|  | import java.io.IOException | ||||||
|  | import java.io.InputStreamReader | ||||||
|  | import java.net.HttpURLConnection | ||||||
|  | import java.net.URI | ||||||
|  |  | ||||||
|  | class CachetAPIController { | ||||||
|  |  | ||||||
|  |      companion object { | ||||||
|  |  | ||||||
|  |          private val logger: Logger = LoggerFactory.getLogger(CachetAPIController::class.java) | ||||||
|  |  | ||||||
|  |          private var oldTotalRequests = 0 | ||||||
|  |  | ||||||
|  |          fun postTotalRequests() { | ||||||
|  |              try { | ||||||
|  |                  val url = URI("${StartupController.cachetBaseURL}/api/v1/metrics/1/points").toURL() | ||||||
|  |                  val jsonInputString = "{\"value\": ${totalRequests -oldTotalRequests}, \"timestamp\": \"${(System.currentTimeMillis() / 1000)}\"}" | ||||||
|  |                  oldTotalRequests = totalRequests | ||||||
|  |  | ||||||
|  |                  val con = url.openConnection() as HttpURLConnection | ||||||
|  |                  con.requestMethod = "POST" | ||||||
|  |                  con.setRequestProperty("Content-Type", "application/json; utf-8") | ||||||
|  |                  con.setRequestProperty("Accept", "application/json") | ||||||
|  |                  con.setRequestProperty("X-Cachet-Token", StartupController.cachetAPIKey) | ||||||
|  |                  con.doOutput = true | ||||||
|  |  | ||||||
|  |                  val os = con.outputStream | ||||||
|  |                  val input = jsonInputString.toByteArray(charset("utf-8")) | ||||||
|  |                  os.write(input, 0, input.size) | ||||||
|  |  | ||||||
|  |                  val br = BufferedReader(InputStreamReader(con.inputStream, "utf-8")) | ||||||
|  |                  val response = StringBuilder() | ||||||
|  |                  var responseLine: String? | ||||||
|  |  | ||||||
|  |                  while (br.readLine().also { responseLine = it } != null) { | ||||||
|  |                      response.append(responseLine!!.trim { it <= ' ' }) | ||||||
|  |                  } | ||||||
|  |                  logger.info(response.toString()) | ||||||
|  |  | ||||||
|  |              } catch (e1: IOException) { | ||||||
|  |                  e1.printStackTrace() | ||||||
|  |              } | ||||||
|  |          } | ||||||
|  |  | ||||||
|  |      } | ||||||
|  |  | ||||||
|  | } | ||||||
| @ -0,0 +1,142 @@ | |||||||
|  | /** | ||||||
|  |  * TheCitadelofRicks | ||||||
|  |  * | ||||||
|  |  * Copyright 2019-2020  <seil0@mosad.xyz> | ||||||
|  |  * | ||||||
|  |  * This program is free software; you can redistribute it and/or modify | ||||||
|  |  * it under the terms of the GNU General Public License as published by | ||||||
|  |  * the Free Software Foundation; either version 3 of the License, or | ||||||
|  |  * (at your option) any later version. | ||||||
|  |  * | ||||||
|  |  * This program is distributed in the hope that it will be useful, | ||||||
|  |  * but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||||
|  |  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||||
|  |  * GNU General Public License for more details. | ||||||
|  |  * | ||||||
|  |  * You should have received a copy of the GNU General Public License | ||||||
|  |  * along with this program; if not, write to the Free Software | ||||||
|  |  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, | ||||||
|  |  * MA 02110-1301, USA. | ||||||
|  |  * | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | package org.mosad.thecitadelofricks.controller | ||||||
|  |  | ||||||
|  | import com.google.gson.Gson | ||||||
|  | import com.google.gson.JsonParser | ||||||
|  | import org.mosad.thecitadelofricks.TimetableCourseWeek | ||||||
|  | import org.slf4j.Logger | ||||||
|  | import org.slf4j.LoggerFactory | ||||||
|  | import java.io.* | ||||||
|  | import java.util.* | ||||||
|  |  | ||||||
|  | class StartupController { | ||||||
|  |  | ||||||
|  |     private val logger: Logger = LoggerFactory.getLogger(CacheController::class.java) | ||||||
|  |  | ||||||
|  |     companion object { | ||||||
|  |         val userHome: String = System.getProperty("user.dir") | ||||||
|  |         val tcorHome = "$userHome/tcor" | ||||||
|  |         val dirTcorHome = File(tcorHome) | ||||||
|  |         val dirTcorCache = File("$tcorHome/cache") | ||||||
|  |         val fileConfig = File("$tcorHome/config.xml") | ||||||
|  |  | ||||||
|  |         var cachetAPIKey = "0" | ||||||
|  |         var cachetBaseURL = "https://status.mosad.xyz" | ||||||
|  |         var courseListURL = "https://www.hs-offenburg.de/studium/vorlesungsplaene/" | ||||||
|  |         val roomListURL = "https://www.hs-offenburg.de/die-hochschule/organisation/infos-services/raumbelegungen" | ||||||
|  |         var mensaMenuURL = "https://www.swfr.de/essen/mensen-cafes-speiseplaene/mensa-offenburg" | ||||||
|  |         var mensaName = "Offenburg" | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     init { | ||||||
|  |         // if the tcor directory doesn't exist, create it | ||||||
|  |         if (!dirTcorHome.exists()) { | ||||||
|  |             dirTcorHome.mkdir() | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         // if the cache directory doesn't exist, create it | ||||||
|  |         if (!dirTcorCache.exists()) { | ||||||
|  |             dirTcorCache.mkdir() | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         // check if the config file exist, if so load it | ||||||
|  |         if (fileConfig.exists()) { | ||||||
|  |             loadConfig() | ||||||
|  |         } else { | ||||||
|  |             createConfig() | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         // read cached timetable files, as they are not initially cached | ||||||
|  |         readCachedTimetables() | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * load the config stored in the config.xml file | ||||||
|  |      */ | ||||||
|  |     private fun loadConfig() = try { | ||||||
|  |         val properties = Properties() | ||||||
|  |         properties.loadFromXML(FileInputStream(fileConfig)) | ||||||
|  |  | ||||||
|  |         try { | ||||||
|  |             cachetAPIKey = properties.getProperty("cachetAPIKey") | ||||||
|  |         } catch (_: Exception) {} | ||||||
|  |  | ||||||
|  |         try { | ||||||
|  |             cachetBaseURL = properties.getProperty("cachetBaseURL") | ||||||
|  |         } catch (_: Exception) {} | ||||||
|  |  | ||||||
|  |         try { | ||||||
|  |             mensaMenuURL = properties.getProperty("mensaMenuURL") | ||||||
|  |         } catch (_: Exception) {} | ||||||
|  |  | ||||||
|  |         try { | ||||||
|  |             mensaName = properties.getProperty("mensaName") | ||||||
|  |         } catch (_: Exception) {} | ||||||
|  |  | ||||||
|  |     } catch (ex: Exception) { | ||||||
|  |         logger.error("error while loading config", ex) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * create an initial config file | ||||||
|  |      */ | ||||||
|  |     private fun createConfig() = try { | ||||||
|  |         val properties = Properties() | ||||||
|  |  | ||||||
|  |         properties.setProperty("cachetAPIKey", cachetAPIKey) | ||||||
|  |         properties.setProperty("cachetBaseURL", cachetBaseURL) | ||||||
|  |         properties.setProperty("mensaMenuURL", mensaMenuURL) | ||||||
|  |         properties.setProperty("mensaName", mensaName) | ||||||
|  |  | ||||||
|  |         val outputStream = FileOutputStream(fileConfig) | ||||||
|  |         properties.storeToXML(outputStream, "tcor configuration") | ||||||
|  |         outputStream.close() | ||||||
|  |     } catch (ex: Exception) { | ||||||
|  |         logger.error("error while creating config", ex) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * read all previously cached timetables | ||||||
|  |      */ | ||||||
|  |     private fun readCachedTimetables() { | ||||||
|  |         dirTcorCache.walkTopDown().forEach { | ||||||
|  |             if (it.isFile && it.name.endsWith(".json")) { | ||||||
|  |                 val fileReader = FileReader(it) | ||||||
|  |                 val bufferedReader = BufferedReader(fileReader) | ||||||
|  |  | ||||||
|  |                 try { | ||||||
|  |                     val timetableObject = JsonParser.parseString(bufferedReader.readLine()).asJsonObject | ||||||
|  |                     val timetable = Gson().fromJson(timetableObject, TimetableCourseWeek().javaClass) | ||||||
|  |                     CacheController.timetableList["${timetable.meta.courseName}-${timetable.meta.weekIndex}"] = | ||||||
|  |                         timetable | ||||||
|  |                 } catch (ex: Exception) { | ||||||
|  |                     logger.error("error while reading cache", ex) | ||||||
|  |                 } finally { | ||||||
|  |                     bufferedReader.close() | ||||||
|  |                     fileReader.close() | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
| @ -0,0 +1,132 @@ | |||||||
|  | /** | ||||||
|  |  * TheCitadelofRicks | ||||||
|  |  * | ||||||
|  |  * Copyright 2019-2020  <seil0@mosad.xyz> | ||||||
|  |  * | ||||||
|  |  * This program is free software; you can redistribute it and/or modify | ||||||
|  |  * it under the terms of the GNU General Public License as published by | ||||||
|  |  * the Free Software Foundation; either version 3 of the License, or | ||||||
|  |  * (at your option) any later version. | ||||||
|  |  * | ||||||
|  |  * This program is distributed in the hope that it will be useful, | ||||||
|  |  * but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||||
|  |  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||||
|  |  * GNU General Public License for more details. | ||||||
|  |  * | ||||||
|  |  * You should have received a copy of the GNU General Public License | ||||||
|  |  * along with this program; if not, write to the Free Software | ||||||
|  |  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, | ||||||
|  |  * MA 02110-1301, USA. | ||||||
|  |  * | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | package org.mosad.thecitadelofricks.controller | ||||||
|  |  | ||||||
|  | import org.mosad.thecitadelofricks.APIController.Companion.apiVersion | ||||||
|  | import org.mosad.thecitadelofricks.APIController.Companion.softwareVersion | ||||||
|  | import org.mosad.thecitadelofricks.APIController.Companion.startTime | ||||||
|  | import org.mosad.thecitadelofricks.Status | ||||||
|  | import org.slf4j.Logger | ||||||
|  | import org.slf4j.LoggerFactory | ||||||
|  | import java.net.HttpURLConnection | ||||||
|  | import java.net.URI | ||||||
|  | import java.net.URL | ||||||
|  | import java.time.LocalDateTime | ||||||
|  | import java.util.* | ||||||
|  | import kotlin.collections.HashMap | ||||||
|  |  | ||||||
|  | class StatusController { | ||||||
|  |  | ||||||
|  |     companion object { | ||||||
|  |         private val logger: Logger = LoggerFactory.getLogger(StatusController::class.java) | ||||||
|  |  | ||||||
|  |         var totalRequests = 0 | ||||||
|  |             private set | ||||||
|  |         var mensaMenuRequests = 0 | ||||||
|  |             private set | ||||||
|  |         var courseListRequests = 0 | ||||||
|  |             private set | ||||||
|  |         var timetableRequests = HashMap<String, Int>() | ||||||
|  |             private set | ||||||
|  |         var roomListRequests = 0 | ||||||
|  |             private set | ||||||
|  |         var roomScheduleRequests = HashMap<String, Int>() | ||||||
|  |             private set | ||||||
|  |  | ||||||
|  |         /** | ||||||
|  |          * if a mensamenu/courseList/timetable is requested update the specific and total request count | ||||||
|  |          */ | ||||||
|  |         fun updateMensaMenuRequests() { | ||||||
|  |             mensaMenuRequests++ | ||||||
|  |             totalRequests++ | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         fun updateCourseListRequests() { | ||||||
|  |             courseListRequests++ | ||||||
|  |             totalRequests++ | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         fun updateTimetableRequests(courseName: String) { | ||||||
|  |             timetableRequests[courseName] = (timetableRequests[courseName] ?: 0) + 1 | ||||||
|  |             totalRequests++ | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         fun updateRoomListRequests() { | ||||||
|  |             roomListRequests++ | ||||||
|  |             totalRequests++ | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         fun updateRoomScheduleRequests(roomName: String) { | ||||||
|  |             roomScheduleRequests[roomName] = (roomScheduleRequests[roomName] ?: 0) + 1 | ||||||
|  |             totalRequests++ | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         fun getStatus(): Status { | ||||||
|  |             val currentTime = System.currentTimeMillis() / 1000 | ||||||
|  |             val minutes = (currentTime - startTime) % 3600 / 60 | ||||||
|  |             val hours = (currentTime - startTime) % 86400 / 3600 | ||||||
|  |             val days = (currentTime - startTime) / 86400 | ||||||
|  |  | ||||||
|  |             var hsoCode = 999 | ||||||
|  |             var swfrCode = 999 | ||||||
|  |  | ||||||
|  |             try { | ||||||
|  |                 val hsoURL = URI("https://www.hs-offenburg.de/").toURL() | ||||||
|  |                 val swfrURL = URI("https://www.swfr.de/").toURL() | ||||||
|  |  | ||||||
|  |                 var connection = hsoURL.openConnection() as HttpURLConnection | ||||||
|  |                 connection.requestMethod = "HEAD" | ||||||
|  |  | ||||||
|  |                 connection.connectTimeout = 15000 | ||||||
|  |                 hsoCode = connection.responseCode | ||||||
|  |  | ||||||
|  |                 connection = swfrURL.openConnection() as HttpURLConnection | ||||||
|  |                 connection.connectTimeout = 15000 | ||||||
|  |                 swfrCode = connection.responseCode | ||||||
|  |             } catch (e: Exception) { | ||||||
|  |                 logger.error("Error while fetching url response codes!", e) | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             return Status( | ||||||
|  |                 LocalDateTime.now(), | ||||||
|  |                 "$days days, $hours:$minutes", | ||||||
|  |                 apiVersion, | ||||||
|  |                 softwareVersion, | ||||||
|  |                 totalRequests, | ||||||
|  |                 mensaMenuRequests, | ||||||
|  |                 courseListRequests, | ||||||
|  |                 timetableRequests, | ||||||
|  |                 CacheController.timetableList.size, | ||||||
|  |                 Date(CacheController.courseList.meta.updateTime * 1000), | ||||||
|  |                 roomListRequests, | ||||||
|  |                 roomScheduleRequests, | ||||||
|  |                 CacheController.roomScheduleList.size, | ||||||
|  |                 Date(CacheController.roomList.meta.updateTime * 1000), | ||||||
|  |                 Date(CacheController.mensaMenu.meta.updateTime * 1000), | ||||||
|  |                 hsoCode, | ||||||
|  |                 swfrCode | ||||||
|  |             ) | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |     } | ||||||
|  | } | ||||||
| @ -1,62 +0,0 @@ | |||||||
| /** |  | ||||||
|  * TheCitadelofRicks |  | ||||||
|  * |  | ||||||
|  * Copyright 2019  <seil0@mosad.xyz> |  | ||||||
|  * |  | ||||||
|  * This program is free software; you can redistribute it and/or modify |  | ||||||
|  * it under the terms of the GNU General Public License as published by |  | ||||||
|  * the Free Software Foundation; either version 3 of the License, or |  | ||||||
|  * (at your option) any later version. |  | ||||||
|  * |  | ||||||
|  * This program is distributed in the hope that it will be useful, |  | ||||||
|  * but WITHOUT ANY WARRANTY; without even the implied warranty of |  | ||||||
|  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the |  | ||||||
|  * GNU General Public License for more details. |  | ||||||
|  * |  | ||||||
|  * You should have received a copy of the GNU General Public License |  | ||||||
|  * along with this program; if not, write to the Free Software |  | ||||||
|  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, |  | ||||||
|  * MA 02110-1301, USA. |  | ||||||
|  * |  | ||||||
|  */ |  | ||||||
|  |  | ||||||
| package org.mosad.thecitadelofricks.hsoparser |  | ||||||
|  |  | ||||||
| import org.jsoup.Jsoup |  | ||||||
| import org.mosad.thecitadelofricks.Course |  | ||||||
| import org.slf4j.LoggerFactory |  | ||||||
| import java.net.SocketTimeoutException |  | ||||||
|  |  | ||||||
| class CourseListParser { |  | ||||||
|  |  | ||||||
|     var logger: org.slf4j.Logger = LoggerFactory.getLogger(MensaParser::class.java) |  | ||||||
|  |  | ||||||
|     /** |  | ||||||
|      * return a list of all courses at courseListURL |  | ||||||
|      * @param courseListURL the url to the course list page |  | ||||||
|      * @return a ArrayList<Course> with all courses or null if the request was not successful |  | ||||||
|      */ |  | ||||||
|     fun getCourseLinks(courseListURL: String): ArrayList<Course>? { |  | ||||||
|         val courseLinkList = ArrayList<Course>() |  | ||||||
|         try { |  | ||||||
|             val courseHTML = Jsoup.connect(courseListURL).get() |  | ||||||
|  |  | ||||||
|             courseHTML.select("ul.index-group").select("li.Class").select("a[href]").forEachIndexed { _, element -> |  | ||||||
|                 courseLinkList.add( |  | ||||||
|                     Course( |  | ||||||
|                         element.text(), |  | ||||||
|                         element.attr("href").replace("http", "https") |  | ||||||
|                     ) |  | ||||||
|                 ) |  | ||||||
|             } |  | ||||||
|         } catch (ex: SocketTimeoutException) { |  | ||||||
|             logger.warn("timeout from hs-offenburg.de, updating on next attempt!") |  | ||||||
|             return null |  | ||||||
|         } catch (gex: Exception) { |  | ||||||
|             logger.error("general CourseListParser error", gex) |  | ||||||
|             return null |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         return courseLinkList |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @ -1,7 +1,7 @@ | |||||||
| /** | /** | ||||||
|  * TheCitadelofRicks |  * TheCitadelofRicks | ||||||
|  * |  * | ||||||
|  * Copyright 2019  <seil0@mosad.xyz> |  * Copyright 2019-2020  <seil0@mosad.xyz> | ||||||
|  * |  * | ||||||
|  * This program is free software; you can redistribute it and/or modify |  * This program is free software; you can redistribute it and/or modify | ||||||
|  * it under the terms of the GNU General Public License as published by |  * it under the terms of the GNU General Public License as published by | ||||||
| @ -24,6 +24,7 @@ package org.mosad.thecitadelofricks.hsoparser | |||||||
|  |  | ||||||
| import org.jsoup.Jsoup | import org.jsoup.Jsoup | ||||||
| import org.jsoup.nodes.Document | import org.jsoup.nodes.Document | ||||||
|  | import org.jsoup.parser.Parser | ||||||
| import org.mosad.thecitadelofricks.Meal | import org.mosad.thecitadelofricks.Meal | ||||||
| import org.mosad.thecitadelofricks.MensaWeek | import org.mosad.thecitadelofricks.MensaWeek | ||||||
| import org.slf4j.LoggerFactory | import org.slf4j.LoggerFactory | ||||||
| @ -59,19 +60,24 @@ class MensaParser { | |||||||
|         val mealWeekList = MensaWeek() |         val mealWeekList = MensaWeek() | ||||||
|  |  | ||||||
|         try { |         try { | ||||||
|             htmlDoc.select("#speiseplan-tabs").select("div.tab-content").select("div.menu-tagesplan") |             htmlDoc.select("#tabsWeekdaysMenu").select("div.menu-tagesplan") | ||||||
|                 .forEachIndexed { dayIndex, day -> |                 .forEachIndexed { dayIndex, day -> | ||||||
|  |                     val meals = mealWeekList.days[dayIndex].meals | ||||||
|                     val strDay = day.select("h3").text() |                     val strDay = day.select("h3").text() | ||||||
|  |  | ||||||
|                     day.select("div.menu-info").forEachIndexed { mealIndex, meal -> |                     day.select("div.menu-tagesplan > div.grid").first()?.select("div.flex-col")?.forEachIndexed { _, meal -> | ||||||
|                         val heading = day.select("h4")[mealIndex].text() |                         val heading = meal.select("h5").text() | ||||||
|                         val parts = ArrayList(meal.html().substringBefore("<br>\n").replace("\n", "").split("<br>")) |                         val parts = ArrayList(meal.select("small.extra-text").html().split("<br>").map { Parser.unescapeEntities(it, true).trim() }) | ||||||
|                         val additives = meal.select("span.show-with-allergenes").text() |                         val additives = meal.select("small.zusatzsstoffe[x-show=showAllergenes]").text() | ||||||
|                         parts.removeIf { x -> x.isEmpty() || x.isBlank() } |                         parts.removeIf { x -> x.isEmpty() || x.isBlank() } | ||||||
|  |  | ||||||
|                         mealWeekList.days[dayIndex].meals.add(Meal(strDay, heading, parts, additives)) |                         meals.add(Meal(strDay, heading, parts, additives)) | ||||||
|                     } |                     } | ||||||
|  |  | ||||||
|  |                     if (meals.isEmpty()) { | ||||||
|  |                         // Add an empty Meal entry to preserve the day information | ||||||
|  |                         meals.add(Meal(strDay, "", ArrayList(), "")) | ||||||
|  |                     } | ||||||
|                 } |                 } | ||||||
|         } catch (pex: Exception) { |         } catch (pex: Exception) { | ||||||
|             logger.error("error while parsing the html file", pex) |             logger.error("error while parsing the html file", pex) | ||||||
| @ -87,8 +93,7 @@ class MensaParser { | |||||||
|      */ |      */ | ||||||
|     fun getMenuLinkNextWeek(mensaMenuURL: String): String { |     fun getMenuLinkNextWeek(mensaMenuURL: String): String { | ||||||
|         val menuHTML = Jsoup.connect(mensaMenuURL).get() |         val menuHTML = Jsoup.connect(mensaMenuURL).get() | ||||||
|  |         return "https://www.swfr.de" + menuHTML.select("div.section-mensa").select("a.next-week").attr("href") | ||||||
|         return "https://www.swfr.de" + menuHTML.select("#speiseplan-tabs").select("a.next-week").attr("href") |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
| } | } | ||||||
| @ -0,0 +1,86 @@ | |||||||
|  | /** | ||||||
|  |  * TheCitadelofRicks | ||||||
|  |  * | ||||||
|  |  * Copyright 2019-2020  <seil0@mosad.xyz> | ||||||
|  |  * | ||||||
|  |  * This program is free software; you can redistribute it and/or modify | ||||||
|  |  * it under the terms of the GNU General Public License as published by | ||||||
|  |  * the Free Software Foundation; either version 3 of the License, or | ||||||
|  |  * (at your option) any later version. | ||||||
|  |  * | ||||||
|  |  * This program is distributed in the hope that it will be useful, | ||||||
|  |  * but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||||
|  |  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||||
|  |  * GNU General Public License for more details. | ||||||
|  |  * | ||||||
|  |  * You should have received a copy of the GNU General Public License | ||||||
|  |  * along with this program; if not, write to the Free Software | ||||||
|  |  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, | ||||||
|  |  * MA 02110-1301, USA. | ||||||
|  |  * | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | package org.mosad.thecitadelofricks.hsoparser | ||||||
|  |  | ||||||
|  | import org.jsoup.Jsoup | ||||||
|  | import org.jsoup.nodes.Element | ||||||
|  | import org.mosad.thecitadelofricks.Course | ||||||
|  | import org.mosad.thecitadelofricks.Room | ||||||
|  | import org.slf4j.LoggerFactory | ||||||
|  | import java.net.SocketTimeoutException | ||||||
|  |  | ||||||
|  | sealed class TimetableLinkListParser<T> { | ||||||
|  |  | ||||||
|  |     private var logger: org.slf4j.Logger = LoggerFactory.getLogger(TimetableLinkListParser::class.java) | ||||||
|  |  | ||||||
|  |     abstract fun constructValue(key: String, link: String): T | ||||||
|  |  | ||||||
|  |     abstract val blacklist: List<String> | ||||||
|  |  | ||||||
|  |     abstract val liClass: String | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * return a list of all elements at listURL | ||||||
|  |      * @param listURL the url to the list page | ||||||
|  |      * @return a ArrayList<T> with all links or null if the request was not successful | ||||||
|  |      */ | ||||||
|  |     fun getLinks(listURL: String): HashMap<String, T>? { | ||||||
|  |         val linkList = HashMap<String, T>() | ||||||
|  |         try { | ||||||
|  |             val courseHTML = Jsoup.connect(listURL).get() | ||||||
|  |  | ||||||
|  |             courseHTML | ||||||
|  |                 .select("ul.index-group") | ||||||
|  |                 .select("li.$liClass") | ||||||
|  |                 .select("a[href]") | ||||||
|  |                 .filter{ it: Element -> !blacklist.contains(it.text()) } | ||||||
|  |                 .forEach { | ||||||
|  |                     linkList[it.text()] = constructValue( | ||||||
|  |                         it.text(), | ||||||
|  |                         it.attr("href").replace("http:", "https:") | ||||||
|  |                     ) | ||||||
|  |                 } | ||||||
|  |             logger.info("successfully retrieved link List") | ||||||
|  |         } catch (ex: SocketTimeoutException) { | ||||||
|  |             logger.warn("timeout from hs-offenburg.de, updating on next attempt!") | ||||||
|  |             return null | ||||||
|  |         } catch (gex: Exception) { | ||||||
|  |             logger.error("general TimetableLinkListParser error", gex) | ||||||
|  |             return null | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         return linkList | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | class CourseListParser : TimetableLinkListParser<Course>() { | ||||||
|  |     override fun constructValue(key: String, link: String) = Course(key, link) | ||||||
|  |     override val blacklist = emptyList<String>() | ||||||
|  |     override val liClass = "Class" | ||||||
|  | } | ||||||
|  |  | ||||||
|  | class RoomListParser : TimetableLinkListParser<Room>() { | ||||||
|  |     override fun constructValue(key: String, link: String) = Room(key, link) | ||||||
|  |     override val blacklist = listOf("STÜBER SYSTEMS") | ||||||
|  |     override val liClass = "Room" | ||||||
|  | } | ||||||
| @ -1,7 +1,7 @@ | |||||||
| /** | /** | ||||||
|  * TheCitadelofRicks |  * TheCitadelofRicks | ||||||
|  * |  * | ||||||
|  * Copyright 2019  <seil0@mosad.xyz> |  * Copyright 2019-2020  <seil0@mosad.xyz> | ||||||
|  * |  * | ||||||
|  * This program is free software; you can redistribute it and/or modify |  * This program is free software; you can redistribute it and/or modify | ||||||
|  * it under the terms of the GNU General Public License as published by |  * it under the terms of the GNU General Public License as published by | ||||||
| @ -22,38 +22,58 @@ | |||||||
|  |  | ||||||
| package org.mosad.thecitadelofricks.hsoparser | package org.mosad.thecitadelofricks.hsoparser | ||||||
|  |  | ||||||
|  | import kotlinx.coroutines.runBlocking | ||||||
|  | import kotlinx.coroutines.sync.Semaphore | ||||||
| import org.jsoup.Jsoup | import org.jsoup.Jsoup | ||||||
| import org.jsoup.nodes.Document | import org.jsoup.nodes.Document | ||||||
| import org.mosad.thecitadelofricks.Lesson | import org.mosad.thecitadelofricks.CalendarWeek | ||||||
|  | import org.mosad.thecitadelofricks.LessonWithCourse | ||||||
|  | import org.mosad.thecitadelofricks.LessonWithRoom | ||||||
| import org.mosad.thecitadelofricks.TimetableWeek | import org.mosad.thecitadelofricks.TimetableWeek | ||||||
| import org.slf4j.LoggerFactory | import org.slf4j.LoggerFactory | ||||||
|  |  | ||||||
| class TimetableParser { | /** | ||||||
|  |  * @param timetableURL the URL of the timetable you want to get | ||||||
|  |  * @param htmlDoc the html document to use (the timetableURL will be ignored if this value is present) | ||||||
|  |  */ | ||||||
|  | sealed class TimetableParser<Lesson>(timetableURL: String? = null, htmlDoc: Document? = null) { | ||||||
|     private var logger: org.slf4j.Logger = LoggerFactory.getLogger(TimetableParser::class.java) |     private var logger: org.slf4j.Logger = LoggerFactory.getLogger(TimetableParser::class.java) | ||||||
|     private val days = arrayOf("Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday") |     private val days = arrayOf("Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday") | ||||||
|  |  | ||||||
|     /** |     abstract fun constructLesson(lessonID: String, lessonSubject: String, lessonTeacher: String, value4: String, lessonRemark: String): Lesson | ||||||
|      * get the timetable from the given url |     abstract val value4Class: String | ||||||
|      * the timetable is organised per row not per column; |  | ||||||
|      * Mon 1, Tue 1, Wed 1, Thur 1, Fri 1, Sat 1, Mon 2 and so on |     companion object { | ||||||
|      * @param timetableURL the URL of the timetable you want to get |         val semaphore = Semaphore(3, 0) | ||||||
|      */ |     } | ||||||
|     fun getTimeTable(timetableURL: String): TimetableWeek { |  | ||||||
|         return try { |     private val htmlDoc: Document? = htmlDoc ?: timetableURL?.let { | ||||||
|             parseTimeTable(Jsoup.connect(timetableURL).get()) |         runBlocking { | ||||||
|         } catch (gex: Exception) { |             try { | ||||||
|             logger.error("general TimetableParser error", gex) |                 // Only allow sending a limited amount of requests at the same time | ||||||
|             TimetableWeek() |                 semaphore.acquire() | ||||||
|  |                 Jsoup.connect(timetableURL).get() | ||||||
|  |             } catch (gex: Exception) { | ||||||
|  |                 logger.error("general TimetableParser error", gex) | ||||||
|  |                 null | ||||||
|  |             } finally { | ||||||
|  |                 semaphore.release() | ||||||
|  |             } | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     fun parseTimeTable(htmlDoc: Document): TimetableWeek { |     /** | ||||||
|         val timetableWeek = TimetableWeek() |      * parse the timetable from the previously given url | ||||||
|         val rows = htmlDoc.select("table.timetable").select("tr[scope=\"row\"]") |      * the timetable is organised per row not per column; | ||||||
|  |      * Mon 1, Tue 1, Wed 1, Thur 1, Fri 1, Sat 1, Mon 2 and so on | ||||||
|  |      */ | ||||||
|  |     fun parseTimeTable(): TimetableWeek<Lesson>? = htmlDoc?.let { | ||||||
|  |         val timetableWeek = TimetableWeek<Lesson>() | ||||||
|  |         val rows = it.select("table.timetable").select("tr[scope=\"row\"]") | ||||||
|  |  | ||||||
|         var sDay = -1 |         var sDay = -1 | ||||||
|         var sRow = -1 |         var sRow = -1 | ||||||
|         var sLesson = Lesson("", "", "", "", "") |         var sLesson = constructLesson("", "", "", "", "") | ||||||
|  |  | ||||||
|         // get each row with index, reflects 1 timeslot per day |         // get each row with index, reflects 1 timeslot per day | ||||||
|         for ((rowIndex, row) in rows.withIndex()) { |         for ((rowIndex, row) in rows.withIndex()) { | ||||||
| @ -61,7 +81,7 @@ class TimetableParser { | |||||||
|             var lessonIndexDay = 0 // the index of the lesson per timeslot |             var lessonIndexDay = 0 // the index of the lesson per timeslot | ||||||
|  |  | ||||||
|             // elements are now all lessons, including empty ones |             // elements are now all lessons, including empty ones | ||||||
|             row.select("td.lastcol, td[style]").forEach {element -> |             row.select("td.lastcol, td[style]").forEach { element -> | ||||||
|  |  | ||||||
|                 // if there is a lecture with rowspan="2", we need to shift everything by one to the left. This is stupid and ugly there needs to bee an API |                 // if there is a lecture with rowspan="2", we need to shift everything by one to the left. This is stupid and ugly there needs to bee an API | ||||||
|                 if ((sDay > -1 && sRow > -1) && (sDay == day && ((sRow + 1) == rowIndex))) { |                 if ((sDay > -1 && sRow > -1) && (sDay == day && ((sRow + 1) == rowIndex))) { | ||||||
| @ -70,11 +90,11 @@ class TimetableParser { | |||||||
|  |  | ||||||
|                     // adjust the following slot |                     // adjust the following slot | ||||||
|                     sDay++ |                     sDay++ | ||||||
|                     sLesson = Lesson( |                     sLesson = constructLesson( | ||||||
|                         "$day.$rowIndex.$lessonIndexDay", |                         "$day.$rowIndex.$lessonIndexDay", | ||||||
|                         element.select("div.lesson-subject").text(), |                         element.select("div.lesson-subject").text(), | ||||||
|                         element.select("div.lesson-teacher").text(), |                         element.select("div.lesson-teacher").text(), | ||||||
|                         element.select("div.lesson-room").text(), |                         element.select("div.$value4Class").text(), | ||||||
|                         element.select("div.lesson-remark").text() |                         element.select("div.lesson-remark").text() | ||||||
|                     ) |                     ) | ||||||
|  |  | ||||||
| @ -85,11 +105,11 @@ class TimetableParser { | |||||||
|  |  | ||||||
|                 } else { |                 } else { | ||||||
|                     timetableWeek.days[day].timeslots[rowIndex].add( |                     timetableWeek.days[day].timeslots[rowIndex].add( | ||||||
|                         Lesson( |                         constructLesson( | ||||||
|                             "$day.$rowIndex.$lessonIndexDay", |                             "$day.$rowIndex.$lessonIndexDay", | ||||||
|                             element.select("div.lesson-subject").text(), |                             element.select("div.lesson-subject").text(), | ||||||
|                             element.select("div.lesson-teacher").text(), |                             element.select("div.lesson-teacher").text(), | ||||||
|                             element.select("div.lesson-room").text(), |                             element.select("div.$value4Class").text(), | ||||||
|                             element.select("div.lesson-remark").text() |                             element.select("div.lesson-remark").text() | ||||||
|                         ) |                         ) | ||||||
|                     ) |                     ) | ||||||
| @ -104,8 +124,7 @@ class TimetableParser { | |||||||
|  |  | ||||||
|                 lessonIndexDay++ |                 lessonIndexDay++ | ||||||
|  |  | ||||||
|                 if (element.hasClass("lastcol")) |                 if (element.hasClass("lastcol")) { | ||||||
|                 { |  | ||||||
|                     day++ |                     day++ | ||||||
|                     lessonIndexDay = 0 |                     lessonIndexDay = 0 | ||||||
|                 } |                 } | ||||||
| @ -117,21 +136,13 @@ class TimetableParser { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * get the week number of the year for the timetable |      * parse the calendar week and the associated year for the timetable | ||||||
|      * @param timetableURL the URL of the timetable you want to get |  | ||||||
|      */ |      */ | ||||||
|     fun getWeekNumberYear(timetableURL: String): Int { |     fun parseCalendarWeek(): CalendarWeek? = htmlDoc?.let { | ||||||
|         return try { |         val dateStr = it.select("h1.timetable-caption").text().substringAfter("- ") | ||||||
|             parseWeekNumberYear(Jsoup.connect(timetableURL).get()) |         val week = dateStr.substringBefore(".").replace(" ", "").toInt() | ||||||
|         } catch (gex: Exception) { |         val year = dateStr.substringAfter("Woche ").replace(" ", "").toInt() | ||||||
|             logger.error("general TimetableParser error", gex) |         CalendarWeek(week, year) | ||||||
|             0 |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     fun parseWeekNumberYear(htmlDoc: Document): Int { |  | ||||||
|         return htmlDoc.select("h1.timetable-caption").text().substringAfter("- ") |  | ||||||
|             .substringBefore(".").replace(" ", "").toInt() |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     @Suppress("unused") |     @Suppress("unused") | ||||||
| @ -139,7 +150,7 @@ class TimetableParser { | |||||||
|      * print a timetable |      * print a timetable | ||||||
|      * @param timetable the timetable to print |      * @param timetable the timetable to print | ||||||
|      */ |      */ | ||||||
|     fun printTimetableWeek(timetable: TimetableWeek) { |     fun printTimetableWeek(timetable: TimetableWeek<LessonWithRoom>) { | ||||||
|         for (j in 0..5) print(days[j].padEnd(75, ' ') + " | ") |         for (j in 0..5) print(days[j].padEnd(75, ' ') + " | ") | ||||||
|         println() |         println() | ||||||
|         for (j in 0..5) print("-".padEnd(76 + (j.toFloat().div(j).toInt()), '-') + "+") |         for (j in 0..5) print("-".padEnd(76 + (j.toFloat().div(j).toInt()), '-') + "+") | ||||||
| @ -181,4 +192,19 @@ class TimetableParser { | |||||||
|  |  | ||||||
|         println(" \n") |         println(" \n") | ||||||
|     } |     } | ||||||
| } |  | ||||||
|  | } | ||||||
|  |  | ||||||
|  | class CourseTimetableParser(timetableURL: String? = null, htmlDoc: Document? = null) : TimetableParser<LessonWithRoom>(timetableURL, htmlDoc) { | ||||||
|  |     override fun constructLesson(lessonID: String, lessonSubject: String, lessonTeacher: String, value4: String, lessonRemark: String) | ||||||
|  |         = LessonWithRoom(lessonID, lessonSubject, lessonTeacher, value4, lessonRemark) | ||||||
|  |  | ||||||
|  |     override val value4Class = "lesson-room" | ||||||
|  | } | ||||||
|  |  | ||||||
|  | class RoomTimetableParser(timetableURL: String? = null, htmlDoc: Document? = null) : TimetableParser<LessonWithCourse>(timetableURL, htmlDoc) { | ||||||
|  |     override fun constructLesson(lessonID: String, lessonSubject: String, lessonTeacher: String, value4: String, lessonRemark: String) | ||||||
|  |             = LessonWithCourse(lessonID, lessonSubject, lessonTeacher, value4, lessonRemark) | ||||||
|  |  | ||||||
|  |     override val value4Class = "lesson-class" | ||||||
|  | } | ||||||
|  | |||||||
| @ -11,5 +11,5 @@ logging.level.org.springframework.web=INFO | |||||||
| # ---------------------------------------- | # ---------------------------------------- | ||||||
|  |  | ||||||
| # EMBEDDED SERVER CONFIGURATION (ServerProperties) | # EMBEDDED SERVER CONFIGURATION (ServerProperties) | ||||||
| server.address=127.0.0.1 | server.address=0.0.0.0 | ||||||
| server.port=8080 | server.port=8080 | ||||||
							
								
								
									
										1726
									
								
								src/main/resources/html/Timetable_normal-week.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1726
									
								
								src/main/resources/html/Timetable_normal-week.html
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							| @ -1,7 +1,7 @@ | |||||||
| /** | /** | ||||||
|  * TheCitadelofRicks |  * TheCitadelofRicks | ||||||
|  * |  * | ||||||
|  * Copyright 2019  <seil0@mosad.xyz> |  * Copyright 2019-2020  <seil0@mosad.xyz> | ||||||
|  * |  * | ||||||
|  * This program is free software; you can redistribute it and/or modify |  * This program is free software; you can redistribute it and/or modify | ||||||
|  * it under the terms of the GNU General Public License as published by |  * it under the terms of the GNU General Public License as published by | ||||||
| @ -28,20 +28,16 @@ import org.junit.jupiter.api.Test | |||||||
| import java.io.File | import java.io.File | ||||||
|  |  | ||||||
| internal class MensaParserTest { | internal class MensaParserTest { | ||||||
|     private val mensaMenuURL = "https://www.swfr.de/de/essen-trinken/speiseplaene/mensa-offenburg/" |     private val mensaMenuURL = "https://www.swfr.de/essen/mensen-cafes-speiseplaene/mensa-offenburg" | ||||||
|  |  | ||||||
|     @Test |     @Test | ||||||
|     fun parseMensaMenuNormalWeek() { |     fun parseMensaMenuNormalWeek() { | ||||||
|         val htmlFile = File(MensaParserTest::class.java.getResource("/html/Mensa_normal-week.html").path) |         val htmlFile = File(MensaParserTest::class.java.getResource("/html/Mensa_normal-week.html").path) | ||||||
|         val htmlDoc = Jsoup.parse(htmlFile,"UTF-8", "https://www.swfr.de/") |         val htmlDoc = Jsoup.parse(htmlFile,"UTF-8", "https://www.swfr.de/") | ||||||
|         val mensaWeek = MensaParser().parseMensaMenu(htmlDoc) |         val mensaWeek = MensaParser().parseMensaMenu(htmlDoc) | ||||||
|  |         val expectedOutput = MensaParserTest::class.java.getResource("/expected/Mensa_normal-week.txt").readText() | ||||||
|  |  | ||||||
|         // maybe we can find another way for the comparison |         Assertions.assertEquals(expectedOutput, mensaWeek.toString()) | ||||||
|         Assertions.assertEquals( |  | ||||||
|             "MensaWeek(days=[Meals(meals=[Meal(day=Montag 03.07., heading=Essen 1, parts=[Gemüseauflauf , Frischkäse-Paprikasauce , Blattsalat <span class=\"zusatzsstoffe hide-with-allergenes\">Kennzeichnungen/Zusatzstoffe: 9</span> <span class=\"zusatzsstoffe show-with-allergenes\">Kennzeichnungen/Zusatzstoffe: 9, enthält Allergene: Ei,ML,Lak</span>], additives=Kennzeichnungen/Zusatzstoffe: 9 enthält Allergene: Ei,ML,Lak), Meal(day=Montag 03.07., heading=Essen 2, parts=[Schweinesteak , Pfeffer-Rahmsauce , Country Potatoes , Blattsalat <span class=\"zusatzsstoffe hide-with-allergenes\">Kennzeichnungen/Zusatzstoffe: 1,5</span> <span class=\"zusatzsstoffe show-with-allergenes\">Kennzeichnungen/Zusatzstoffe: 1,5, enthält Allergene: Gl,GlW,GlG,ML,Lak</span>], additives=Kennzeichnungen/Zusatzstoffe: 1,5 enthält Allergene: Gl,GlW,GlG,ML,Lak), Meal(day=Montag 03.07., heading=Buffet, parts=[Grill und Salatbar , Asiatisches aus dem Wok , Fleisch- und Fischspezialitäten , Pasta-, Reis und Kartoffelvariationen , Gemüse], additives=)]), Meals(meals=[Meal(day=Dienstag 04.07., heading=Essen 1, parts=[Riesenrösti , Rahmchampignons , Blattsalat <span class=\"zusatzsstoffe show-with-allergenes\">enthält Allergene: Gl,GlW,GlG,ML,Lak</span>], additives=enthält Allergene: Gl,GlW,GlG,ML,Lak), Meal(day=Dienstag 04.07., heading=Essen 2, parts=[Spaghetti , Sauce Bolognese , Geriebener Hartkäse , Blattsalat <span class=\"zusatzsstoffe hide-with-allergenes\">Kennzeichnungen/Zusatzstoffe: 15</span> <span class=\"zusatzsstoffe show-with-allergenes\">Kennzeichnungen/Zusatzstoffe: 15, enthält Allergene: Ei,Se,Gl,GlW,ML</span>], additives=Kennzeichnungen/Zusatzstoffe: 15 enthält Allergene: Ei,Se,Gl,GlW,ML), Meal(day=Dienstag 04.07., heading=Buffet, parts=[Grill und Salatbar , Asiatisches aus dem Wok , Fleisch- und Fischspezialitäten , Pasta-, Reis und Kartoffelvariationen , Gemüse], additives=)]), Meals(meals=[Meal(day=Mittwoch 05.07., heading=Essen 1, parts=[Frühlingsrollen , Feuriger Chilidip , Patnareis , Wokgemüsepfanne <span class=\"zusatzsstoffe hide-with-allergenes\">Kennzeichnungen/Zusatzstoffe: 9</span> <span class=\"zusatzsstoffe show-with-allergenes\">Kennzeichnungen/Zusatzstoffe: 9, enthält Allergene: Ei,So,Sn,Se,Sf,Gl,GlW,ML,Lak</span>], additives=Kennzeichnungen/Zusatzstoffe: 9 enthält Allergene: Ei,So,Sn,Se,Sf,Gl,GlW,ML,Lak), Meal(day=Mittwoch 05.07., heading=Essen 2, parts=[Bratwurstschnecke , Bratenjus , Kartoffelbrei , Karottengemüse <span class=\"zusatzsstoffe hide-with-allergenes\">Kennzeichnungen/Zusatzstoffe: 1,8</span> <span class=\"zusatzsstoffe show-with-allergenes\">Kennzeichnungen/Zusatzstoffe: 1,8, enthält Allergene: Sn,Se,Gl,GlW,GlG,ML,Lak</span>], additives=Kennzeichnungen/Zusatzstoffe: 1,8 enthält Allergene: Sn,Se,Gl,GlW,GlG,ML,Lak), Meal(day=Mittwoch 05.07., heading=Buffet, parts=[Grill und Salatbar , Asiatisches aus dem Wok , Fleisch- und Fischspezialitäten , Pasta-, Reis und Kartoffelvariationen , Gemüse], additives=)]), Meals(meals=[Meal(day=Donnerstag 06.07., heading=Essen 1, parts=[Farfalle tricolore , Gorgonzola-Spinatsauce , Endiviensalat <span class=\"zusatzsstoffe show-with-allergenes\">enthält Allergene: Gl,GlW,ML</span>], additives=enthält Allergene: Gl,GlW,ML), Meal(day=Donnerstag 06.07., heading=Essen 2, parts=[Paniertes Seelachsfilet MSC , Remouladensauce , Salzkartoffeln , Brokkoligemüse <span class=\"zusatzsstoffe hide-with-allergenes\">Kennzeichnungen/Zusatzstoffe: 9,MSC</span> <span class=\"zusatzsstoffe show-with-allergenes\">Kennzeichnungen/Zusatzstoffe: 9,MSC, enthält Allergene: Ei,Sn,Fi,Gl,GlW,ML,Lak</span>], additives=Kennzeichnungen/Zusatzstoffe: 9,MSC enthält Allergene: Ei,Sn,Fi,Gl,GlW,ML,Lak), Meal(day=Donnerstag 06.07., heading=Buffet, parts=[Grill und Salatbar , Asiatisches aus dem Wok , Fleisch- und Fischspezialitäten , Pasta-, Reis und Kartoffelvariationen , Gemüse], additives=)]), Meals(meals=[Meal(day=Freitag 07.07., heading=Essen 1, parts=[Italienischer Nudelauflauf mit Gemüse , Tomatenragout , Blattsalat <span class=\"zusatzsstoffe show-with-allergenes\">enthält Allergene: Gl,GlW,ML,Lak</span>], additives=enthält Allergene: Gl,GlW,ML,Lak), Meal(day=Freitag 07.07., heading=Essen 2, parts=[Hähnchen-Saté-Spieß , Erdnusssauce , Langkornreis , Karotten-Erbsengemüse <span class=\"zusatzsstoffe show-with-allergenes\">enthält Allergene: Er,So,Kr,Fi,We,Gl,GlW,ML,Lak</span>], additives=enthält Allergene: Er,So,Kr,Fi,We,Gl,GlW,ML,Lak), Meal(day=Freitag 07.07., heading=Buffet, parts=[Grill und Salatbar , Asiatisches aus dem Wok , Fleisch- und Fischspezialitäten , Pasta-, Reis und Kartoffelvariationen , Gemüse], additives=)]), Meals(meals=[]), Meals(meals=[])])", |  | ||||||
|             mensaWeek.toString() |  | ||||||
|         ) |  | ||||||
|  |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     @Test |     @Test | ||||||
| @ -49,11 +45,9 @@ internal class MensaParserTest { | |||||||
|         val htmlFile = File(MensaParserTest::class.java.getResource("/html/Mensa_empty-week.html").path) |         val htmlFile = File(MensaParserTest::class.java.getResource("/html/Mensa_empty-week.html").path) | ||||||
|         val htmlDoc = Jsoup.parse(htmlFile,"UTF-8", "https://www.swfr.de/") |         val htmlDoc = Jsoup.parse(htmlFile,"UTF-8", "https://www.swfr.de/") | ||||||
|         val mensaWeek = MensaParser().parseMensaMenu(htmlDoc) |         val mensaWeek = MensaParser().parseMensaMenu(htmlDoc) | ||||||
|  |         val expectedOutput = MensaParserTest::class.java.getResource("/expected/Mensa_empty-week.txt").readText() | ||||||
|  |  | ||||||
|         Assertions.assertEquals( |         Assertions.assertEquals(expectedOutput, mensaWeek.toString()) | ||||||
|             "MensaWeek(days=[Meals(meals=[]), Meals(meals=[]), Meals(meals=[]), Meals(meals=[]), Meals(meals=[]), Meals(meals=[]), Meals(meals=[])])", |  | ||||||
|             mensaWeek.toString() |  | ||||||
|         ) |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     // TODO add test for special days ie. public holiday |     // TODO add test for special days ie. public holiday | ||||||
| @ -63,4 +57,4 @@ internal class MensaParserTest { | |||||||
|         val urlNextWeek = MensaParser().getMenuLinkNextWeek(mensaMenuURL) // this need a connection to the swfr server |         val urlNextWeek = MensaParser().getMenuLinkNextWeek(mensaMenuURL) // this need a connection to the swfr server | ||||||
|         Assertions.assertNotNull(urlNextWeek) |         Assertions.assertNotNull(urlNextWeek) | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  | |||||||
| @ -1,7 +1,7 @@ | |||||||
| /** | /** | ||||||
|  * TheCitadelofRicks |  * TheCitadelofRicks | ||||||
|  * |  * | ||||||
|  * Copyright 2019  <seil0@mosad.xyz> |  * Copyright 2019-2020  <seil0@mosad.xyz> | ||||||
|  * |  * | ||||||
|  * This program is free software; you can redistribute it and/or modify |  * This program is free software; you can redistribute it and/or modify | ||||||
|  * it under the terms of the GNU General Public License as published by |  * it under the terms of the GNU General Public License as published by | ||||||
| @ -25,6 +25,7 @@ package org.mosad.thecitadelofricks.hsoparser | |||||||
| import org.jsoup.Jsoup | import org.jsoup.Jsoup | ||||||
| import org.junit.jupiter.api.Assertions | import org.junit.jupiter.api.Assertions | ||||||
| import org.junit.jupiter.api.Test | import org.junit.jupiter.api.Test | ||||||
|  | import org.mosad.thecitadelofricks.CalendarWeek | ||||||
| import java.io.File | import java.io.File | ||||||
|  |  | ||||||
| class TimetableParserTest { | class TimetableParserTest { | ||||||
| @ -32,9 +33,9 @@ class TimetableParserTest { | |||||||
|     @Test |     @Test | ||||||
|     fun parseTimetableNormalWeek() { |     fun parseTimetableNormalWeek() { | ||||||
|         val htmlFile = File(TimetableParserTest::class.java.getResource("/html/Timetable_normal-week.html").path) |         val htmlFile = File(TimetableParserTest::class.java.getResource("/html/Timetable_normal-week.html").path) | ||||||
|         val htmlDoc = Jsoup.parse(htmlFile,"UTF-8", "https://www.hs-offenburg.de/") |         val htmlDoc = Jsoup.parse(htmlFile, "UTF-8", "https://www.hs-offenburg.de/") | ||||||
|         val actualTimetable = TimetableParser().parseTimeTable(htmlDoc).toString().trim() |         val actualTimetable = CourseTimetableParser(htmlDoc = htmlDoc).parseTimeTable().toString().trim() | ||||||
|         val expectedTimetable = File(TimetableParserTest::class.java.getResource("/expected/Timetable_normal-week_expected.txt").toURI()).readText().trim() |         val expectedTimetable = TimetableParserTest::class.java.getResource("/expected/Timetable_normal-week.txt").readText().trim() | ||||||
|  |  | ||||||
|         Assertions.assertEquals(expectedTimetable, actualTimetable) |         Assertions.assertEquals(expectedTimetable, actualTimetable) | ||||||
|     } |     } | ||||||
| @ -42,19 +43,19 @@ class TimetableParserTest { | |||||||
|     @Test |     @Test | ||||||
|     fun parseTimetableEmptyWeek() { |     fun parseTimetableEmptyWeek() { | ||||||
|         val htmlFile = File(TimetableParserTest::class.java.getResource("/html/Timetable_empty-week.html").path) |         val htmlFile = File(TimetableParserTest::class.java.getResource("/html/Timetable_empty-week.html").path) | ||||||
|         val htmlDoc = Jsoup.parse(htmlFile,"UTF-8", "https://www.hs-offenburg.de/") |         val htmlDoc = Jsoup.parse(htmlFile, "UTF-8", "https://www.hs-offenburg.de/") | ||||||
|         val actualTimetable = TimetableParser().parseTimeTable(htmlDoc).toString().trim() |         val actualTimetable = CourseTimetableParser(htmlDoc = htmlDoc).parseTimeTable().toString().trim() | ||||||
|         val expectedTimetable = File(TimetableParserTest::class.java.getResource("/expected/Timetable_empty-week_expected.txt").toURI()).readText().trim() |         val expectedTimetable = TimetableParserTest::class.java.getResource("/expected/Timetable_empty-week.txt").readText().trim() | ||||||
|  |  | ||||||
|         Assertions.assertEquals(expectedTimetable, actualTimetable) |         Assertions.assertEquals(expectedTimetable, actualTimetable) | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     @Test |     @Test | ||||||
|     fun parseWeekNumberYear() { |     fun parseCalendarWeek() { | ||||||
|         val htmlFile = File(TimetableParserTest::class.java.getResource("/html/Timetable_normal-week.html").path) |         val htmlFile = File(TimetableParserTest::class.java.getResource("/html/Timetable_normal-week.html").path) | ||||||
|         val htmlDoc = Jsoup.parse(htmlFile,"UTF-8", "https://www.hs-offenburg.de/") |         val htmlDoc = Jsoup.parse(htmlFile, "UTF-8", "https://www.hs-offenburg.de/") | ||||||
|         val actualWeekNumberYear = TimetableParser().parseWeekNumberYear(htmlDoc) |         val actualCalendarWeek = CourseTimetableParser(htmlDoc = htmlDoc).parseCalendarWeek() | ||||||
|  |  | ||||||
|         Assertions.assertEquals(42, actualWeekNumberYear) |         Assertions.assertEquals(CalendarWeek(42, 2019), actualCalendarWeek) | ||||||
|     } |     } | ||||||
| } | } | ||||||
							
								
								
									
										1
									
								
								src/test/resources/expected/Mensa_empty-week.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								src/test/resources/expected/Mensa_empty-week.txt
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1 @@ | |||||||
|  | MensaWeek(days=[Meals(meals=[Meal(day=Montag 26.12., heading=, parts=[], additives=)]), Meals(meals=[Meal(day=Dienstag 27.12., heading=, parts=[], additives=)]), Meals(meals=[Meal(day=Mittwoch 28.12., heading=, parts=[], additives=)]), Meals(meals=[Meal(day=Donnerstag 29.12., heading=, parts=[], additives=)]), Meals(meals=[Meal(day=Freitag 30.12., heading=, parts=[], additives=)]), Meals(meals=[Meal(day=Samstag 31.12., heading=, parts=[], additives=)]), Meals(meals=[])]) | ||||||
							
								
								
									
										1
									
								
								src/test/resources/expected/Mensa_normal-week.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								src/test/resources/expected/Mensa_normal-week.txt
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1 @@ | |||||||
|  | MensaWeek(days=[Meals(meals=[Meal(day=Montag 19.12., heading=Essen 1, parts=[auf Wunsch vegan, Gemüselasagne oder, Cannelloni mit Gemüse und Käsekruste, Tomatensauce, Beilagensalat oder Regio-Apfel], additives=Kennzeichnungen/Zusatzstoffe: 5 enthält Allergene: ML,GlW,Gl,Ei,Se), Meal(day=Montag 19.12., heading=Essen 2, parts=[Grönsaksbullar -Gemüsebällchen oder, Köttbullar, Pfeffer-Rahmsauce, Blumenkohl-Kartoffelpüree, Beilagensalat oder Regio-Apfel], additives=Kennzeichnungen/Zusatzstoffe: 5,sch,ri,4 enthält Allergene: ML,GlW,Gl,Ei)]), Meals(meals=[Meal(day=Dienstag 20.12., heading=Essen 1, parts=[Brokkoli-Nussecken, Rotweinsauce, Kartoffel-Kürbispüree, Apfelrotkraut, Feldsalat], additives=Kennzeichnungen/Zusatzstoffe: 5 enthält Allergene: GlW,GlH,ML,NM,Gl,Sf,Se,Nu,NH), Meal(day=Dienstag 20.12., heading=Essen 2, parts=[Weihnachtsessen, Hirschgulasch, Rotweinsauce, Bauernspätzle, Apfelrotkraut, Feldsalat], additives=Kennzeichnungen/Zusatzstoffe: 5 enthält Allergene: Ei,Gl,GlW,ML,Sf)]), Meals(meals=[Meal(day=Mittwoch 21.12., heading=Essen 1, parts=[Gemüsenuggets, Mojo Dip, Gitterkartoffeln, Beilagensalat oder Regio-Apfel], additives=Kennzeichnungen/Zusatzstoffe: 5 enthält Allergene: GlW,GlG,Gl,ML), Meal(day=Mittwoch 21.12., heading=Essen 2, parts=[Gebratener Gemüsereis Nasi Goreng, Bio Tofu-Erdnusswürfel, Beilagensalat oder Regio-Apfel], additives=Kennzeichnungen/Zusatzstoffe: o enthält Allergene: Gl,GlW,Se,Sf,So,Sn)]), Meals(meals=[Meal(day=Donnerstag 22.12., heading=Essen 1, parts=[Schupfnudel-Gemüsepfanne, Weiße Sauce, Beilagensalat oder Regio-Apfel], additives=enthält Allergene: Gl,GlW,Se,So), Meal(day=Donnerstag 22.12., heading=Essen 2, parts=[Fischragout, Bandnudeln, Buntes Marktgemüse], additives=Kennzeichnungen/Zusatzstoffe: nF enthält Allergene: Se,ML,GlW,Gl,Ei,Fi)]), Meals(meals=[Meal(day=Freitag 23.12., heading=, parts=[], additives=)]), Meals(meals=[Meal(day=Samstag 24.12., heading=, parts=[], additives=)]), Meals(meals=[])]) | ||||||
							
								
								
									
										1
									
								
								src/test/resources/expected/Timetable_normal-week.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								src/test/resources/expected/Timetable_normal-week.txt
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1 @@ | |||||||
|  | TimetableWeek(days=[TimetableDay(timeslots=[[LessonWithRoom(lessonID=0.0.0, lessonSubject=, lessonTeacher=, lessonRoom=, lessonRemark=)], [LessonWithRoom(lessonID=0.1.0, lessonSubject=, lessonTeacher=, lessonRoom=, lessonRemark=)], [LessonWithRoom(lessonID=0.2.0, lessonSubject=, lessonTeacher=, lessonRoom=, lessonRemark=)], [LessonWithRoom(lessonID=0.3.0, lessonSubject=Prakt. Computernetze, lessonTeacher=Mayer E., lessonRoom=STB 0.28, A111a, lessonRemark=)], [LessonWithRoom(lessonID=0.4.0, lessonSubject=Prakt. Computernetze, lessonTeacher=Mayer E., lessonRoom=STB 0.28, A111a, lessonRemark=)], [LessonWithRoom(lessonID=0.5.0, lessonSubject=, lessonTeacher=, lessonRoom=, lessonRemark=)]]), TimetableDay(timeslots=[[LessonWithRoom(lessonID=1.0.0, lessonSubject=, lessonTeacher=, lessonRoom=, lessonRemark=)], [LessonWithRoom(lessonID=1.1.0, lessonSubject=Systemprogramm., lessonTeacher=Mayer E., lessonRoom=B040, lessonRemark=)], [LessonWithRoom(lessonID=1.2.0, lessonSubject=, lessonTeacher=, lessonRoom=, lessonRemark=)], [LessonWithRoom(lessonID=1.3.0, lessonSubject=Prakt. Computernetze, lessonTeacher=Mayer E., lessonRoom=STB 0.28, A112, lessonRemark=)], [LessonWithRoom(lessonID=1.4.0, lessonSubject=Prakt. Computernetze, lessonTeacher=Mayer E., lessonRoom=STB 0.28, A112, lessonRemark=)], [LessonWithRoom(lessonID=1.5.0, lessonSubject=, lessonTeacher=, lessonRoom=, lessonRemark=)]]), TimetableDay(timeslots=[[LessonWithRoom(lessonID=2.0.0, lessonSubject=Softw. Engin. 1, lessonTeacher=Dorer, K., lessonRoom=A213, lessonRemark=)], [LessonWithRoom(lessonID=2.1.0, lessonSubject=Datenbanksysteme 1, lessonTeacher=Grabowski, H., lessonRoom=B040, lessonRemark=)], [LessonWithRoom(lessonID=2.2.0, lessonSubject=, lessonTeacher=, lessonRoom=, lessonRemark=)], [LessonWithRoom(lessonID=2.3.0, lessonSubject=Praktikum Internet-Programmierung, lessonTeacher=Orb, J., lessonRoom=B207, lessonRemark=)], [LessonWithRoom(lessonID=2.4.0, lessonSubject=Praktikum Internet-Programmierung, lessonTeacher=Orb, J., lessonRoom=B207, lessonRemark=)], [LessonWithRoom(lessonID=2.5.0, lessonSubject=, lessonTeacher=, lessonRoom=, lessonRemark=)]]), TimetableDay(timeslots=[[LessonWithRoom(lessonID=3.0.0, lessonSubject=Prakt. Datenbanksyst., lessonTeacher=Grabowski, H., lessonRoom=D115, B204b, lessonRemark=)], [LessonWithRoom(lessonID=3.1.0, lessonSubject=Zusatzübung C++, lessonTeacher=Behr, A., lessonRoom=D116, lessonRemark=), LessonWithRoom(lessonID=3.1.1, lessonSubject=Prakt. Datenbanksyst., lessonTeacher=Grabowski, H., lessonRoom=D115, B204b, lessonRemark=)], [LessonWithRoom(lessonID=3.2.0, lessonSubject=Softw. Engin. 1, lessonTeacher=Dorer, K., lessonRoom=B013, lessonRemark=)], [LessonWithRoom(lessonID=3.3.0, lessonSubject=, lessonTeacher=, lessonRoom=, lessonRemark=)], [LessonWithRoom(lessonID=3.4.0, lessonSubject=, lessonTeacher=, lessonRoom=, lessonRemark=)], [LessonWithRoom(lessonID=3.5.0, lessonSubject=, lessonTeacher=, lessonRoom=, lessonRemark=)]]), TimetableDay(timeslots=[[LessonWithRoom(lessonID=4.0.0, lessonSubject=Computernetze 1, lessonTeacher=Mayer E., lessonRoom=A213, lessonRemark=)], [LessonWithRoom(lessonID=4.1.0, lessonSubject=Internet-Programmierung, lessonTeacher=Orb, J., lessonRoom=A213, lessonRemark=)], [LessonWithRoom(lessonID=4.2.0, lessonSubject=Prakt. Systemprogr., lessonTeacher=Mayer E., lessonRoom=STB 0.28, lessonRemark=)], [LessonWithRoom(lessonID=4.3.0, lessonSubject=Prakt. Systemprogr., lessonTeacher=Mayer E., lessonRoom=STB 0.28, lessonRemark=)], [LessonWithRoom(lessonID=4.4.0, lessonSubject=, lessonTeacher=, lessonRoom=, lessonRemark=)], [LessonWithRoom(lessonID=4.5.0, lessonSubject=, lessonTeacher=, lessonRoom=, lessonRemark=)]]), TimetableDay(timeslots=[[LessonWithRoom(lessonID=5.0.0, lessonSubject=, lessonTeacher=, lessonRoom=, lessonRemark=)], [LessonWithRoom(lessonID=5.1.0, lessonSubject=, lessonTeacher=, lessonRoom=, lessonRemark=)], [LessonWithRoom(lessonID=5.2.0, lessonSubject=, lessonTeacher=, lessonRoom=, lessonRemark=)], [LessonWithRoom(lessonID=5.3.0, lessonSubject=, lessonTeacher=, lessonRoom=, lessonRemark=)], [LessonWithRoom(lessonID=5.4.0, lessonSubject=, lessonTeacher=, lessonRoom=, lessonRemark=)], [LessonWithRoom(lessonID=5.5.0, lessonSubject=, lessonTeacher=, lessonRoom=, lessonRemark=)]])]) | ||||||
| @ -1 +0,0 @@ | |||||||
| TimetableWeek(days=[TimetableDay(timeslots=[[Lesson(lessonID=0.0.0, lessonSubject=, lessonTeacher=, lessonRoom=, lessonRemark=)], [Lesson(lessonID=0.1.0, lessonSubject=, lessonTeacher=, lessonRoom=, lessonRemark=)], [Lesson(lessonID=0.2.0, lessonSubject=, lessonTeacher=, lessonRoom=, lessonRemark=)], [Lesson(lessonID=0.3.0, lessonSubject=Prakt. Computernetze, lessonTeacher=Mayer E., lessonRoom=STB 0.28, A111a, lessonRemark=)], [Lesson(lessonID=0.4.0, lessonSubject=Prakt. Computernetze, lessonTeacher=Mayer E., lessonRoom=STB 0.28, A111a, lessonRemark=)], [Lesson(lessonID=0.5.0, lessonSubject=, lessonTeacher=, lessonRoom=, lessonRemark=)]]), TimetableDay(timeslots=[[Lesson(lessonID=1.0.0, lessonSubject=, lessonTeacher=, lessonRoom=, lessonRemark=)], [Lesson(lessonID=1.1.0, lessonSubject=Systemprogramm., lessonTeacher=Mayer E., lessonRoom=B040, lessonRemark=)], [Lesson(lessonID=1.2.0, lessonSubject=, lessonTeacher=, lessonRoom=, lessonRemark=)], [Lesson(lessonID=1.3.0, lessonSubject=Prakt. Computernetze, lessonTeacher=Mayer E., lessonRoom=STB 0.28, A112, lessonRemark=)], [Lesson(lessonID=1.4.0, lessonSubject=Prakt. Computernetze, lessonTeacher=Mayer E., lessonRoom=STB 0.28, A112, lessonRemark=)], [Lesson(lessonID=1.5.0, lessonSubject=, lessonTeacher=, lessonRoom=, lessonRemark=)]]), TimetableDay(timeslots=[[Lesson(lessonID=2.0.0, lessonSubject=Softw. Engin. 1, lessonTeacher=Dorer, K., lessonRoom=A213, lessonRemark=)], [Lesson(lessonID=2.1.0, lessonSubject=Datenbanksysteme 1, lessonTeacher=Grabowski, H., lessonRoom=B040, lessonRemark=)], [Lesson(lessonID=2.2.0, lessonSubject=, lessonTeacher=, lessonRoom=, lessonRemark=)], [Lesson(lessonID=2.3.0, lessonSubject=Praktikum Internet-Programmierung, lessonTeacher=Orb, J., lessonRoom=B207, lessonRemark=)], [Lesson(lessonID=2.4.0, lessonSubject=Praktikum Internet-Programmierung, lessonTeacher=Orb, J., lessonRoom=B207, lessonRemark=)], [Lesson(lessonID=2.5.0, lessonSubject=, lessonTeacher=, lessonRoom=, lessonRemark=)]]), TimetableDay(timeslots=[[Lesson(lessonID=3.0.0, lessonSubject=Prakt. Datenbanksyst., lessonTeacher=Grabowski, H., lessonRoom=D115, B204b, lessonRemark=)], [Lesson(lessonID=3.1.0, lessonSubject=Zusatzübung C++, lessonTeacher=Behr, A., lessonRoom=D116, lessonRemark=), Lesson(lessonID=3.1.1, lessonSubject=Prakt. Datenbanksyst., lessonTeacher=Grabowski, H., lessonRoom=D115, B204b, lessonRemark=)], [Lesson(lessonID=3.2.0, lessonSubject=Softw. Engin. 1, lessonTeacher=Dorer, K., lessonRoom=B013, lessonRemark=)], [Lesson(lessonID=3.3.0, lessonSubject=, lessonTeacher=, lessonRoom=, lessonRemark=)], [Lesson(lessonID=3.4.0, lessonSubject=, lessonTeacher=, lessonRoom=, lessonRemark=)], [Lesson(lessonID=3.5.0, lessonSubject=, lessonTeacher=, lessonRoom=, lessonRemark=)]]), TimetableDay(timeslots=[[Lesson(lessonID=4.0.0, lessonSubject=Computernetze 1, lessonTeacher=Mayer E., lessonRoom=A213, lessonRemark=)], [Lesson(lessonID=4.1.0, lessonSubject=Internet-Programmierung, lessonTeacher=Orb, J., lessonRoom=A213, lessonRemark=)], [Lesson(lessonID=4.2.0, lessonSubject=Prakt. Systemprogr., lessonTeacher=Mayer E., lessonRoom=STB 0.28, lessonRemark=)], [Lesson(lessonID=4.3.0, lessonSubject=Prakt. Systemprogr., lessonTeacher=Mayer E., lessonRoom=STB 0.28, lessonRemark=)], [Lesson(lessonID=4.4.0, lessonSubject=, lessonTeacher=, lessonRoom=, lessonRemark=)], [Lesson(lessonID=4.5.0, lessonSubject=, lessonTeacher=, lessonRoom=, lessonRemark=)]]), TimetableDay(timeslots=[[Lesson(lessonID=5.0.0, lessonSubject=, lessonTeacher=, lessonRoom=, lessonRemark=)], [Lesson(lessonID=5.1.0, lessonSubject=, lessonTeacher=, lessonRoom=, lessonRemark=)], [Lesson(lessonID=5.2.0, lessonSubject=, lessonTeacher=, lessonRoom=, lessonRemark=)], [Lesson(lessonID=5.3.0, lessonSubject=, lessonTeacher=, lessonRoom=, lessonRemark=)], [Lesson(lessonID=5.4.0, lessonSubject=, lessonTeacher=, lessonRoom=, lessonRemark=)], [Lesson(lessonID=5.5.0, lessonSubject=, lessonTeacher=, lessonRoom=, lessonRemark=)]])]) |  | ||||||
| @ -1,134 +1,105 @@ | |||||||
| <div id="speiseplan-tabs"> | <div id="tabsWeekdaysMenu"> | ||||||
|     <div id="tab-menu-container" class="row"> |              | ||||||
|         <div class="col-md-1 col-xs-6"></div> |                 <div id="tab-mon" class="menu-tagesplan" role="tabpanel" aria-labelledby="label-mon" x-show="currentDay == 1"> | ||||||
|         <div class="col-md-10"> |                     <h3>Montag 26.12.</h3> | ||||||
|             <ul class="nav classic-tabs tabs-primary" role="tablist"> |  | ||||||
|                 <li class="col-md-2"><a href="#tab-mon" class="nav-link" data-toggle="tab" role="tab">Mo 12.08.</a></li> |                      | ||||||
|                 <li class="col-md-2"><a href="#tab-tue" class="nav-link" data-toggle="tab" role="tab">Di 13.08.</a></li> |  | ||||||
|                 <li class="col-md-2"><a href="#tab-wed" class="nav-link" data-toggle="tab" role="tab">Mi 14.08.</a></li> |                      | ||||||
|                 <li class="col-md-2"><a href="#tab-thu" class="nav-link" data-toggle="tab" role="tab">Do 15.08.</a></li> |                             <div class="row row-narrow row-buffer row-table"> | ||||||
|                 <li class="col-md-2"><a href="#tab-fri" class="nav-link active show" data-toggle="tab" role="tab">Fr |                                 <div class="col-md-1 bg-beige-hell zusatzangaben"> | ||||||
|                     16.08.</a></li> |                                     <i class="glyphicons glyphicons-circle-info"></i> | ||||||
|                 <li class="col-md-2"><a href="#tab-sat" class="nav-link" data-toggle="tab" role="tab">Sa 17.08.</a></li> |                                 </div> | ||||||
|             </ul> |                                 <div class="col-md-11 border-beige-hell"> | ||||||
|         </div> |                                     <h4>heute keine Essensausgabe</h4> | ||||||
|         <div class="col-md-1 col-xs-6 pull-right"> |                                 </div> | ||||||
|             <a class="next-week text-right" title="eine Woche weiter" |                             </div> | ||||||
|                href="https://www.swfr.de/de/essen-trinken/speiseplaene/mensa-offenburg/?tx_swfrspeiseplan_pi1%5BweekToShow%5D=1&tx_swfrspeiseplan_pi1%5Baction%5D=show&tx_swfrspeiseplan_pi1%5Bcontroller%5D=Speiseplan&cHash=fab2e8b1bae3cdd249ace0e5d67c1c7e"><span |                          | ||||||
|                     class="d-inline d-sm-none">nächste Woche</span><img |  | ||||||
|                     src="Mensa_empty-week-Dateien/keil-grau-rechts.png" alt="" width="16" height="16"></a> |  | ||||||
|         </div> |  | ||||||
|     </div> |  | ||||||
|     <div class="row"> |  | ||||||
|         <div class="col-sm-4"> |  | ||||||
|             <table class="table legende"> |  | ||||||
|                 <tbody> |  | ||||||
|                 <tr> |  | ||||||
|                     <td class="vegetarisch legend"></td> |  | ||||||
|                     <td class="legend-info">vegetarisch</td> |  | ||||||
|                 </tr> |  | ||||||
|                 <tr> |  | ||||||
|                     <td class="vegan legend"></td> |  | ||||||
|                     <td class="legend-info">vegan</td> |  | ||||||
|                 </tr> |  | ||||||
|                 <tr> |  | ||||||
|                     <td class="wunsch-vegan legend"></td> |  | ||||||
|                     <td class="legend-info">auf Wunsch vegan</td> |  | ||||||
|                 </tr> |  | ||||||
|                 </tbody> |  | ||||||
|             </table> |  | ||||||
|         </div> |  | ||||||
|         <div class="col-sm-4 pt-3"> |  | ||||||
|             <div class="hide-with-allergenes"> |  | ||||||
|                 <p><a download="_KW_33-2019.pdf" title="Wochenplan in Farbe herunterladen" target="_blank" |  | ||||||
|                       href="https://www.swfr.de/de/essen-trinken/speiseplaene/mensa-offenburg/?type=99&tx_swfrspeiseplan_pi1%5BweekToShow%5D=0&tx_swfrspeiseplan_pi1%5Bcolored%5D=1&tx_swfrspeiseplan_pi1%5Bort%5D=651&tx_swfrspeiseplan_pi1%5Baction%5D=buildPdf&tx_swfrspeiseplan_pi1%5Bcontroller%5D=Speiseplan&cHash=9c8a69897ba4f187d21b350d48473425"><i |  | ||||||
|                         class="fas fa-file-download" aria-hidden="true"></i> Wochenplan farbig</a></p> |  | ||||||
|                 <p><a download="_KW_33-2019.pdf" title="Wochenplan in schwarz-weiß herunterladen" target="_blank" |  | ||||||
|                       href="https://www.swfr.de/de/essen-trinken/speiseplaene/mensa-offenburg/?type=99&tx_swfrspeiseplan_pi1%5BweekToShow%5D=0&tx_swfrspeiseplan_pi1%5Bcolored%5D=0&tx_swfrspeiseplan_pi1%5Bort%5D=651&tx_swfrspeiseplan_pi1%5Baction%5D=buildPdf&tx_swfrspeiseplan_pi1%5Bcontroller%5D=Speiseplan&cHash=3e887f29d8cf2a15bd54620c56f43e02"><i |  | ||||||
|                         class="fas fa-file-download" aria-hidden="true"></i> Wochenplan s/w</a></p> |  | ||||||
|             </div> |  | ||||||
|             <div class="show-with-allergenes"> |  | ||||||
|                 <p><a download="_KW_33-2019.pdf" title="Wochenplan in Farbe herunterladen" target="_blank" |  | ||||||
|                       href="https://www.swfr.de/de/essen-trinken/speiseplaene/mensa-offenburg/?type=99&tx_swfrspeiseplan_pi1%5BweekToShow%5D=0&tx_swfrspeiseplan_pi1%5Bcolored%5D=1&tx_swfrspeiseplan_pi1%5Bort%5D=651&tx_swfrspeiseplan_pi1%5Baction%5D=buildPdfAllergenes&tx_swfrspeiseplan_pi1%5Bcontroller%5D=Speiseplan&cHash=e5c36a88733c75c5eded775acad35bc6"><i |  | ||||||
|                         class="fas fa-file-download" aria-hidden="true"></i> Wochenplan farbig</a></p> |  | ||||||
|                 <p><a download="_KW_33-2019.pdf" title="Wochenplan in schwarz-weiß herunterladen" target="_blank" |  | ||||||
|                       href="https://www.swfr.de/de/essen-trinken/speiseplaene/mensa-offenburg/?type=99&tx_swfrspeiseplan_pi1%5BweekToShow%5D=0&tx_swfrspeiseplan_pi1%5Bcolored%5D=0&tx_swfrspeiseplan_pi1%5Bort%5D=651&tx_swfrspeiseplan_pi1%5Baction%5D=buildPdfAllergenes&tx_swfrspeiseplan_pi1%5Bcontroller%5D=Speiseplan&cHash=b0d4e30323a2010310bbb6cad8b55fe9"><i |  | ||||||
|                         class="fas fa-file-download" aria-hidden="true"></i> Wochenplan s/w</a></p> |  | ||||||
|             </div> |  | ||||||
|         </div> |  | ||||||
|         <div class="col-sm-4 pt-3"> |  | ||||||
|             <p class="hide-with-allergenes"><a href="#" data-toggle="modal" data-target="#allergenesModal"><i |  | ||||||
|                     class="fas fa-redo-alt"></i> Allergiehinweise anzeigen</a></p> |  | ||||||
|             <p class="show-with-allergenes"><a href="#" class="btn-hide-allergenes"><i class="fas fa-undo-alt"></i> |  | ||||||
|                 Allergiehinweise verbergen</a></p> |  | ||||||
|         </div> |  | ||||||
|     </div> |  | ||||||
|     <div class="tab-content"> |  | ||||||
|         <div id="tab-mon" class="menu-tagesplan tab-pane fade"> |  | ||||||
|             <h3>Montag 12.08.</h3> |  | ||||||
|             <div class="row row-narrow row-buffer row-table"> |  | ||||||
|                 <div class="col-md-1 bg-beige-hell zusatzangaben"> |  | ||||||
|                     <i class="glyphicons glyphicons-circle-info"></i> |  | ||||||
|                 </div> |                 </div> | ||||||
|                 <div class="col-md-11 border-beige-hell"> |              | ||||||
|                     <h4>heute keine Essensausgabe</h4> |                 <div id="tab-tue" class="menu-tagesplan" role="tabpanel" aria-labelledby="label-tue" x-show="currentDay == 2"> | ||||||
|  |                     <h3>Dienstag 27.12.</h3> | ||||||
|  |  | ||||||
|  |                      | ||||||
|  |  | ||||||
|  |                      | ||||||
|  |                             <div class="row row-narrow row-buffer row-table"> | ||||||
|  |                                 <div class="col-md-1 bg-beige-hell zusatzangaben"> | ||||||
|  |                                     <i class="glyphicons glyphicons-circle-info"></i> | ||||||
|  |                                 </div> | ||||||
|  |                                 <div class="col-md-11 border-beige-hell"> | ||||||
|  |                                     <h4>heute keine Essensausgabe</h4> | ||||||
|  |                                 </div> | ||||||
|  |                             </div> | ||||||
|  |                          | ||||||
|                 </div> |                 </div> | ||||||
|             </div> |              | ||||||
|         </div> |                 <div id="tab-wed" class="menu-tagesplan" role="tabpanel" aria-labelledby="label-wed" x-show="currentDay == 3"> | ||||||
|         <div id="tab-tue" class="menu-tagesplan tab-pane fade"> |                     <h3>Mittwoch 28.12.</h3> | ||||||
|             <h3>Dienstag 13.08.</h3> |  | ||||||
|             <div class="row row-narrow row-buffer row-table"> |                      | ||||||
|                 <div class="col-md-1 bg-beige-hell zusatzangaben"> |  | ||||||
|                     <i class="glyphicons glyphicons-circle-info"></i> |                      | ||||||
|  |                             <div class="row row-narrow row-buffer row-table"> | ||||||
|  |                                 <div class="col-md-1 bg-beige-hell zusatzangaben"> | ||||||
|  |                                     <i class="glyphicons glyphicons-circle-info"></i> | ||||||
|  |                                 </div> | ||||||
|  |                                 <div class="col-md-11 border-beige-hell"> | ||||||
|  |                                     <h4>heute keine Essensausgabe</h4> | ||||||
|  |                                 </div> | ||||||
|  |                             </div> | ||||||
|  |                          | ||||||
|                 </div> |                 </div> | ||||||
|                 <div class="col-md-11 border-beige-hell"> |              | ||||||
|                     <h4>heute keine Essensausgabe</h4> |                 <div id="tab-thu" class="menu-tagesplan" role="tabpanel" aria-labelledby="label-thu" x-show="currentDay == 4"> | ||||||
|  |                     <h3>Donnerstag 29.12.</h3> | ||||||
|  |  | ||||||
|  |                      | ||||||
|  |  | ||||||
|  |                      | ||||||
|  |                             <div class="row row-narrow row-buffer row-table"> | ||||||
|  |                                 <div class="col-md-1 bg-beige-hell zusatzangaben"> | ||||||
|  |                                     <i class="glyphicons glyphicons-circle-info"></i> | ||||||
|  |                                 </div> | ||||||
|  |                                 <div class="col-md-11 border-beige-hell"> | ||||||
|  |                                     <h4>heute keine Essensausgabe</h4> | ||||||
|  |                                 </div> | ||||||
|  |                             </div> | ||||||
|  |                          | ||||||
|                 </div> |                 </div> | ||||||
|             </div> |              | ||||||
|         </div> |                 <div id="tab-fri" class="menu-tagesplan" role="tabpanel" aria-labelledby="label-fri" x-show="currentDay == 5"> | ||||||
|         <div id="tab-wed" class="menu-tagesplan tab-pane fade"> |                     <h3>Freitag 30.12.</h3> | ||||||
|             <h3>Mittwoch 14.08.</h3> |  | ||||||
|             <div class="row row-narrow row-buffer row-table"> |                      | ||||||
|                 <div class="col-md-1 bg-beige-hell zusatzangaben"> |  | ||||||
|                     <i class="glyphicons glyphicons-circle-info"></i> |                      | ||||||
|  |                             <div class="row row-narrow row-buffer row-table"> | ||||||
|  |                                 <div class="col-md-1 bg-beige-hell zusatzangaben"> | ||||||
|  |                                     <i class="glyphicons glyphicons-circle-info"></i> | ||||||
|  |                                 </div> | ||||||
|  |                                 <div class="col-md-11 border-beige-hell"> | ||||||
|  |                                     <h4>heute keine Essensausgabe</h4> | ||||||
|  |                                 </div> | ||||||
|  |                             </div> | ||||||
|  |                          | ||||||
|                 </div> |                 </div> | ||||||
|                 <div class="col-md-11 border-beige-hell"> |              | ||||||
|                     <h4>heute keine Essensausgabe</h4> |                 <div id="tab-sat" class="menu-tagesplan" role="tabpanel" aria-labelledby="label-sat" x-show="currentDay == 6"> | ||||||
|  |                     <h3>Samstag 31.12.</h3> | ||||||
|  |  | ||||||
|  |                      | ||||||
|  |  | ||||||
|  |                      | ||||||
|  |                             <div class="row row-narrow row-buffer row-table"> | ||||||
|  |                                 <div class="col-md-1 bg-beige-hell zusatzangaben"> | ||||||
|  |                                     <i class="glyphicons glyphicons-circle-info"></i> | ||||||
|  |                                 </div> | ||||||
|  |                                 <div class="col-md-11 border-beige-hell"> | ||||||
|  |                                     <h4>heute keine Essensausgabe</h4> | ||||||
|  |                                 </div> | ||||||
|  |                             </div> | ||||||
|  |                          | ||||||
|                 </div> |                 </div> | ||||||
|             </div> |              | ||||||
|         </div> |         </div> | ||||||
|         <div id="tab-thu" class="menu-tagesplan tab-pane fade"> |  | ||||||
|             <h3>Donnerstag 15.08.</h3> |  | ||||||
|             <div class="row row-narrow row-buffer row-table"> |  | ||||||
|                 <div class="col-md-1 bg-beige-hell zusatzangaben"> |  | ||||||
|                     <i class="glyphicons glyphicons-circle-info"></i> |  | ||||||
|                 </div> |  | ||||||
|                 <div class="col-md-11 border-beige-hell"> |  | ||||||
|                     <h4>heute keine Essensausgabe</h4> |  | ||||||
|                 </div> |  | ||||||
|             </div> |  | ||||||
|         </div> |  | ||||||
|         <div id="tab-fri" class="menu-tagesplan tab-pane fade in show active"> |  | ||||||
|             <h3>Freitag 16.08.</h3> |  | ||||||
|             <div class="row row-narrow row-buffer row-table"> |  | ||||||
|                 <div class="col-md-1 bg-beige-hell zusatzangaben"> |  | ||||||
|                     <i class="glyphicons glyphicons-circle-info"></i> |  | ||||||
|                 </div> |  | ||||||
|                 <div class="col-md-11 border-beige-hell"> |  | ||||||
|                     <h4>heute keine Essensausgabe</h4> |  | ||||||
|                 </div> |  | ||||||
|             </div> |  | ||||||
|         </div> |  | ||||||
|         <div id="tab-sat" class="menu-tagesplan tab-pane fade"> |  | ||||||
|             <h3>Samstag 17.08.</h3> |  | ||||||
|             <div class="row row-narrow row-buffer row-table"> |  | ||||||
|                 <div class="col-md-1 bg-beige-hell zusatzangaben"> |  | ||||||
|                     <i class="glyphicons glyphicons-circle-info"></i> |  | ||||||
|                 </div> |  | ||||||
|                 <div class="col-md-11 border-beige-hell"> |  | ||||||
|                     <h4>heute keine Essensausgabe</h4> |  | ||||||
|                 </div> |  | ||||||
|             </div> |  | ||||||
|         </div> |  | ||||||
|     </div> |  | ||||||
| </div> |  | ||||||
| @ -1,518 +1,467 @@ | |||||||
| <div id="speiseplan-tabs"> | <div id="tabsWeekdaysMenu"> | ||||||
|     <div id="tab-menu-container" class="row"> |              | ||||||
|         <div class="col-md-1 col-xs-6"></div> |                 <div id="tab-mon" class="menu-tagesplan" role="tabpanel" aria-labelledby="label-mon" x-show="currentDay == 1"> | ||||||
|         <div class="col-md-10"> |                     <h3>Montag 19.12.</h3> | ||||||
|             <ul class="nav classic-tabs tabs-primary" role="tablist"> |  | ||||||
|                 <li class="col-md-2"><a href="#tab-mon" class="nav-link" data-toggle="tab" role="tab">Mo 12.08.</a></li> |  | ||||||
|                 <li class="col-md-2"><a href="#tab-tue" class="nav-link" data-toggle="tab" role="tab">Di 13.08.</a></li> |  | ||||||
|                 <li class="col-md-2"><a href="#tab-wed" class="nav-link" data-toggle="tab" role="tab">Mi 14.08.</a></li> |  | ||||||
|                 <li class="col-md-2"><a href="#tab-thu" class="nav-link" data-toggle="tab" role="tab">Do 15.08.</a></li> |  | ||||||
|                 <li class="col-md-2"><a href="#tab-fri" class="nav-link active show" data-toggle="tab" role="tab">Fr |  | ||||||
|                     16.08.</a></li> |  | ||||||
|                 <li class="col-md-2"><a href="#tab-sat" class="nav-link" data-toggle="tab" role="tab">Sa 17.08.</a></li> |  | ||||||
|             </ul> |  | ||||||
|         </div> |  | ||||||
|         <div class="col-md-1 col-xs-6 pull-right"> |  | ||||||
|             <a class="next-week text-right" title="eine Woche weiter" |  | ||||||
|                href="https://www.swfr.de/de/essen-trinken/speiseplaene/mensa-offenburg/?tx_swfrspeiseplan_pi1%5BweekToShow%5D=1&tx_swfrspeiseplan_pi1%5Baction%5D=show&tx_swfrspeiseplan_pi1%5Bcontroller%5D=Speiseplan&cHash=fab2e8b1bae3cdd249ace0e5d67c1c7e"><span |  | ||||||
|                     class="d-inline d-sm-none">nächste Woche</span><img |  | ||||||
|                     src="Mensa_empty-week-Dateien/keil-grau-rechts.png" alt="" width="16" height="16"></a> |  | ||||||
|         </div> |  | ||||||
|     </div> |  | ||||||
|     <div class="row"> |  | ||||||
|         <div class="col-sm-4"> |  | ||||||
|             <table class="table legende"> |  | ||||||
|                 <tbody> |  | ||||||
|                 <tr> |  | ||||||
|                     <td class="vegetarisch legend"></td> |  | ||||||
|                     <td class="legend-info">vegetarisch</td> |  | ||||||
|                 </tr> |  | ||||||
|                 <tr> |  | ||||||
|                     <td class="vegan legend"></td> |  | ||||||
|                     <td class="legend-info">vegan</td> |  | ||||||
|                 </tr> |  | ||||||
|                 <tr> |  | ||||||
|                     <td class="wunsch-vegan legend"></td> |  | ||||||
|                     <td class="legend-info">auf Wunsch vegan</td> |  | ||||||
|                 </tr> |  | ||||||
|                 </tbody> |  | ||||||
|             </table> |  | ||||||
|         </div> |  | ||||||
|         <div class="col-sm-4 pt-3"> |  | ||||||
|             <div class="hide-with-allergenes"> |  | ||||||
|                 <p><a download="_KW_33-2019.pdf" title="Wochenplan in Farbe herunterladen" target="_blank" |  | ||||||
|                       href="https://www.swfr.de/de/essen-trinken/speiseplaene/mensa-offenburg/?type=99&tx_swfrspeiseplan_pi1%5BweekToShow%5D=0&tx_swfrspeiseplan_pi1%5Bcolored%5D=1&tx_swfrspeiseplan_pi1%5Bort%5D=651&tx_swfrspeiseplan_pi1%5Baction%5D=buildPdf&tx_swfrspeiseplan_pi1%5Bcontroller%5D=Speiseplan&cHash=9c8a69897ba4f187d21b350d48473425"><i |  | ||||||
|                         class="fas fa-file-download" aria-hidden="true"></i> Wochenplan farbig</a></p> |  | ||||||
|                 <p><a download="_KW_33-2019.pdf" title="Wochenplan in schwarz-weiß herunterladen" target="_blank" |  | ||||||
|                       href="https://www.swfr.de/de/essen-trinken/speiseplaene/mensa-offenburg/?type=99&tx_swfrspeiseplan_pi1%5BweekToShow%5D=0&tx_swfrspeiseplan_pi1%5Bcolored%5D=0&tx_swfrspeiseplan_pi1%5Bort%5D=651&tx_swfrspeiseplan_pi1%5Baction%5D=buildPdf&tx_swfrspeiseplan_pi1%5Bcontroller%5D=Speiseplan&cHash=3e887f29d8cf2a15bd54620c56f43e02"><i |  | ||||||
|                         class="fas fa-file-download" aria-hidden="true"></i> Wochenplan s/w</a></p> |  | ||||||
|             </div> |  | ||||||
|             <div class="show-with-allergenes"> |  | ||||||
|                 <p><a download="_KW_33-2019.pdf" title="Wochenplan in Farbe herunterladen" target="_blank" |  | ||||||
|                       href="https://www.swfr.de/de/essen-trinken/speiseplaene/mensa-offenburg/?type=99&tx_swfrspeiseplan_pi1%5BweekToShow%5D=0&tx_swfrspeiseplan_pi1%5Bcolored%5D=1&tx_swfrspeiseplan_pi1%5Bort%5D=651&tx_swfrspeiseplan_pi1%5Baction%5D=buildPdfAllergenes&tx_swfrspeiseplan_pi1%5Bcontroller%5D=Speiseplan&cHash=e5c36a88733c75c5eded775acad35bc6"><i |  | ||||||
|                         class="fas fa-file-download" aria-hidden="true"></i> Wochenplan farbig</a></p> |  | ||||||
|                 <p><a download="_KW_33-2019.pdf" title="Wochenplan in schwarz-weiß herunterladen" target="_blank" |  | ||||||
|                       href="https://www.swfr.de/de/essen-trinken/speiseplaene/mensa-offenburg/?type=99&tx_swfrspeiseplan_pi1%5BweekToShow%5D=0&tx_swfrspeiseplan_pi1%5Bcolored%5D=0&tx_swfrspeiseplan_pi1%5Bort%5D=651&tx_swfrspeiseplan_pi1%5Baction%5D=buildPdfAllergenes&tx_swfrspeiseplan_pi1%5Bcontroller%5D=Speiseplan&cHash=b0d4e30323a2010310bbb6cad8b55fe9"><i |  | ||||||
|                         class="fas fa-file-download" aria-hidden="true"></i> Wochenplan s/w</a></p> |  | ||||||
|             </div> |  | ||||||
|         </div> |  | ||||||
|         <div class="col-sm-4 pt-3"> |  | ||||||
|             <p class="hide-with-allergenes"><a href="#" data-toggle="modal" data-target="#allergenesModal"><i |  | ||||||
|                     class="fas fa-redo-alt"></i> Allergiehinweise anzeigen</a></p> |  | ||||||
|             <p class="show-with-allergenes"><a href="#" class="btn-hide-allergenes"><i class="fas fa-undo-alt"></i> |  | ||||||
|                 Allergiehinweise verbergen</a></p> |  | ||||||
|         </div> |  | ||||||
|     </div> |  | ||||||
|     <div class="tab-content"> |  | ||||||
|         <div id="tab-mon" class="menu-tagesplan r-tabs-panel r-tabs-state-active" style="display: block;"> |  | ||||||
|             <h3>Montag 03.07.</h3> |  | ||||||
|             <div class="row row-narrow row-table"> |  | ||||||
|                 <h4 class="col-md-3 col-sm-3 menu-header">Essen 1</h4> |  | ||||||
|                 <div class="col-md-5 col-sm-5 menu-info bg-beige-hell vegetarisch"> |  | ||||||
|                     Gemüseauflauf |  | ||||||
|                     <br>Frischkäse-Paprikasauce |  | ||||||
|                     <br>Blattsalat |  | ||||||
|                     <span class="zusatzsstoffe hide-with-allergenes">Kennzeichnungen/Zusatzstoffe: 9</span> |  | ||||||
|                     <span class="zusatzsstoffe show-with-allergenes">Kennzeichnungen/Zusatzstoffe: 9<br>enthält Allergene: Ei,ML,Lak</span> |  | ||||||
|                 </div> |  | ||||||
|                 <div class="col-md-4 col-sm-4 menu-price"> |  | ||||||
|                     <table class="table"> |  | ||||||
|                         <tbody> |  | ||||||
|                         <tr> |  | ||||||
|                             <td class="price-studierende">Studierende, Schüler</td> |  | ||||||
|                             <td>2,90 €</td> |  | ||||||
|                         </tr> |  | ||||||
|                         <tr> |  | ||||||
|                             <td class="price-mitarbeiter">Mitarbeiter</td> |  | ||||||
|                             <td>3,80 €</td> |  | ||||||
|                         </tr> |  | ||||||
|                         <tr> |  | ||||||
|                             <td class="price-gaeste">Gäste</td> |  | ||||||
|                             <td>5,10 €</td> |  | ||||||
|                         </tr> |  | ||||||
|                         </tbody> |  | ||||||
|                     </table> |  | ||||||
|                 </div> |  | ||||||
|             </div> |  | ||||||
|             <div class="row row-narrow row-table"> |  | ||||||
|                 <h4 class="col-md-3 col-sm-3 menu-header">Essen 2</h4> |  | ||||||
|                 <div class="col-md-5 col-sm-5 menu-info bg-beige-hell "> |  | ||||||
|                     Schweinesteak |  | ||||||
|                     <br>Pfeffer-Rahmsauce |  | ||||||
|                     <br>Country Potatoes |  | ||||||
|                     <br>Blattsalat |  | ||||||
|                     <span class="zusatzsstoffe hide-with-allergenes">Kennzeichnungen/Zusatzstoffe: 1,5</span> |  | ||||||
|                     <span class="zusatzsstoffe show-with-allergenes">Kennzeichnungen/Zusatzstoffe: 1,5<br>enthält Allergene: Gl,GlW,GlG,ML,Lak</span> |  | ||||||
|                 </div> |  | ||||||
|                 <div class="col-md-4 col-sm-4 menu-price"> |  | ||||||
|                     <table class="table"> |  | ||||||
|                         <tbody> |  | ||||||
|                         <tr> |  | ||||||
|                             <td class="price-studierende">Studierende, Schüler</td> |  | ||||||
|                             <td>2,90 €</td> |  | ||||||
|                         </tr> |  | ||||||
|                         <tr> |  | ||||||
|                             <td class="price-mitarbeiter">Mitarbeiter</td> |  | ||||||
|                             <td>3,80 €</td> |  | ||||||
|                         </tr> |  | ||||||
|                         <tr> |  | ||||||
|                             <td class="price-gaeste">Gäste</td> |  | ||||||
|                             <td>5,10 €</td> |  | ||||||
|                         </tr> |  | ||||||
|                         </tbody> |  | ||||||
|                     </table> |  | ||||||
|                 </div> |  | ||||||
|             </div> |  | ||||||
|             <div class="row row-narrow row-table"> |  | ||||||
|                 <h4 class="col-md-3 col-sm-3 menu-header">Buffet</h4> |  | ||||||
|                 <div class="col-md-5 col-sm-5 menu-info bg-beige-hell "> |  | ||||||
|                     Grill und Salatbar |  | ||||||
|                     <br>Asiatisches aus dem Wok |  | ||||||
|                     <br>Fleisch- und Fischspezialitäten |  | ||||||
|                     <br>Pasta-, Reis und Kartoffelvariationen |  | ||||||
|                     <br>Gemüse |  | ||||||
|                 </div> |  | ||||||
|                 <div class="col-md-4 col-sm-4 menu-price"> |  | ||||||
|                     <table class="table"> |  | ||||||
|                         <tbody> |  | ||||||
|                         <tr> |  | ||||||
|                             <td class="price-studierende">Studierende, Schüler</td> |  | ||||||
|                             <td>0,85 €<span class="tara"> / 100g</span></td> |  | ||||||
|                         </tr> |  | ||||||
|                         <tr> |  | ||||||
|                             <td class="price-mitarbeiter">Mitarbeiter</td> |  | ||||||
|                             <td>1,00 €<span class="tara"> / 100g</span></td> |  | ||||||
|                         </tr> |  | ||||||
|                         <tr> |  | ||||||
|                             <td class="price-gaeste">Gäste</td> |  | ||||||
|                             <td>1,20 €<span class="tara"> / 100g</span></td> |  | ||||||
|                         </tr> |  | ||||||
|                         </tbody> |  | ||||||
|                     </table> |  | ||||||
|                 </div> |  | ||||||
|             </div> |  | ||||||
|         </div> |  | ||||||
|         <div id="tab-tue" class="menu-tagesplan r-tabs-panel r-tabs-state-default" style="display: none;"> |  | ||||||
|             <h3>Dienstag 04.07.</h3> |  | ||||||
|             <div class="row row-narrow row-table"> |  | ||||||
|                 <h4 class="col-md-3 col-sm-3 menu-header">Essen 1</h4> |  | ||||||
|                 <div class="col-md-5 col-sm-5 menu-info bg-beige-hell vegetarisch"> |  | ||||||
|                     Riesenrösti |  | ||||||
|                     <br>Rahmchampignons |  | ||||||
|                     <br>Blattsalat |  | ||||||
|                     <span class="zusatzsstoffe show-with-allergenes">enthält Allergene: Gl,GlW,GlG,ML,Lak</span> |  | ||||||
|                 </div> |  | ||||||
|                 <div class="col-md-4 col-sm-4 menu-price"> |  | ||||||
|                     <table class="table"> |  | ||||||
|                         <tbody> |  | ||||||
|                         <tr> |  | ||||||
|                             <td class="price-studierende">Studierende, Schüler</td> |  | ||||||
|                             <td>2,90 €</td> |  | ||||||
|                         </tr> |  | ||||||
|                         <tr> |  | ||||||
|                             <td class="price-mitarbeiter">Mitarbeiter</td> |  | ||||||
|                             <td>3,80 €</td> |  | ||||||
|                         </tr> |  | ||||||
|                         <tr> |  | ||||||
|                             <td class="price-gaeste">Gäste</td> |  | ||||||
|                             <td>5,10 €</td> |  | ||||||
|                         </tr> |  | ||||||
|                         </tbody> |  | ||||||
|                     </table> |  | ||||||
|                 </div> |  | ||||||
|             </div> |  | ||||||
|             <div class="row row-narrow row-table"> |  | ||||||
|                 <h4 class="col-md-3 col-sm-3 menu-header">Essen 2</h4> |  | ||||||
|                 <div class="col-md-5 col-sm-5 menu-info bg-beige-hell "> |  | ||||||
|                     Spaghetti |  | ||||||
|                     <br>Sauce Bolognese |  | ||||||
|                     <br>Geriebener Hartkäse |  | ||||||
|                     <br>Blattsalat |  | ||||||
|                     <span class="zusatzsstoffe hide-with-allergenes">Kennzeichnungen/Zusatzstoffe: 15</span> |  | ||||||
|                     <span class="zusatzsstoffe show-with-allergenes">Kennzeichnungen/Zusatzstoffe: 15<br>enthält Allergene: Ei,Se,Gl,GlW,ML</span> |  | ||||||
|                 </div> |  | ||||||
|                 <div class="col-md-4 col-sm-4 menu-price"> |  | ||||||
|                     <table class="table"> |  | ||||||
|                         <tbody> |  | ||||||
|                         <tr> |  | ||||||
|                             <td class="price-studierende">Studierende, Schüler</td> |  | ||||||
|                             <td>2,90 €</td> |  | ||||||
|                         </tr> |  | ||||||
|                         <tr> |  | ||||||
|                             <td class="price-mitarbeiter">Mitarbeiter</td> |  | ||||||
|                             <td>3,80 €</td> |  | ||||||
|                         </tr> |  | ||||||
|                         <tr> |  | ||||||
|                             <td class="price-gaeste">Gäste</td> |  | ||||||
|                             <td>5,10 €</td> |  | ||||||
|                         </tr> |  | ||||||
|                         </tbody> |  | ||||||
|                     </table> |  | ||||||
|                 </div> |  | ||||||
|             </div> |  | ||||||
|             <div class="row row-narrow row-table"> |  | ||||||
|                 <h4 class="col-md-3 col-sm-3 menu-header">Buffet</h4> |  | ||||||
|                 <div class="col-md-5 col-sm-5 menu-info bg-beige-hell "> |  | ||||||
|                     Grill und Salatbar |  | ||||||
|                     <br>Asiatisches aus dem Wok |  | ||||||
|                     <br>Fleisch- und Fischspezialitäten |  | ||||||
|                     <br>Pasta-, Reis und Kartoffelvariationen |  | ||||||
|                     <br>Gemüse |  | ||||||
|                 </div> |  | ||||||
|                 <div class="col-md-4 col-sm-4 menu-price"> |  | ||||||
|                     <table class="table"> |  | ||||||
|                         <tbody> |  | ||||||
|                         <tr> |  | ||||||
|                             <td class="price-studierende">Studierende, Schüler</td> |  | ||||||
|                             <td>0,85 €<span class="tara"> / 100g</span></td> |  | ||||||
|                         </tr> |  | ||||||
|                         <tr> |  | ||||||
|                             <td class="price-mitarbeiter">Mitarbeiter</td> |  | ||||||
|                             <td>1,00 €<span class="tara"> / 100g</span></td> |  | ||||||
|                         </tr> |  | ||||||
|                         <tr> |  | ||||||
|                             <td class="price-gaeste">Gäste</td> |  | ||||||
|                             <td>1,20 €<span class="tara"> / 100g</span></td> |  | ||||||
|                         </tr> |  | ||||||
|                         </tbody> |  | ||||||
|                     </table> |  | ||||||
|                 </div> |  | ||||||
|             </div> |  | ||||||
|         </div> |  | ||||||
|         <div id="tab-wed" class="menu-tagesplan r-tabs-panel r-tabs-state-default" style="display: none;"> |  | ||||||
|             <h3>Mittwoch 05.07.</h3> |  | ||||||
|             <div class="row row-narrow row-table"> |  | ||||||
|                 <h4 class="col-md-3 col-sm-3 menu-header">Essen 1</h4> |  | ||||||
|                 <div class="col-md-5 col-sm-5 menu-info bg-beige-hell vegetarisch"> |  | ||||||
|                     Frühlingsrollen |  | ||||||
|                     <br>Feuriger Chilidip |  | ||||||
|                     <br>Patnareis |  | ||||||
|                     <br>Wokgemüsepfanne |  | ||||||
|                     <span class="zusatzsstoffe hide-with-allergenes">Kennzeichnungen/Zusatzstoffe: 9</span> |  | ||||||
|                     <span class="zusatzsstoffe show-with-allergenes">Kennzeichnungen/Zusatzstoffe: 9<br>enthält Allergene: Ei,So,Sn,Se,Sf,Gl,GlW,ML,Lak</span> |  | ||||||
|                 </div> |  | ||||||
|                 <div class="col-md-4 col-sm-4 menu-price"> |  | ||||||
|                     <table class="table"> |  | ||||||
|                         <tbody> |  | ||||||
|                         <tr> |  | ||||||
|                             <td class="price-studierende">Studierende, Schüler</td> |  | ||||||
|                             <td>2,90 €</td> |  | ||||||
|                         </tr> |  | ||||||
|                         <tr> |  | ||||||
|                             <td class="price-mitarbeiter">Mitarbeiter</td> |  | ||||||
|                             <td>3,80 €</td> |  | ||||||
|                         </tr> |  | ||||||
|                         <tr> |  | ||||||
|                             <td class="price-gaeste">Gäste</td> |  | ||||||
|                             <td>5,10 €</td> |  | ||||||
|                         </tr> |  | ||||||
|                         </tbody> |  | ||||||
|                     </table> |  | ||||||
|                 </div> |  | ||||||
|             </div> |  | ||||||
|             <div class="row row-narrow row-table"> |  | ||||||
|                 <h4 class="col-md-3 col-sm-3 menu-header">Essen 2</h4> |  | ||||||
|                 <div class="col-md-5 col-sm-5 menu-info bg-beige-hell "> |  | ||||||
|                     Bratwurstschnecke |  | ||||||
|                     <br>Bratenjus |  | ||||||
|                     <br>Kartoffelbrei |  | ||||||
|                     <br>Karottengemüse |  | ||||||
|                     <span class="zusatzsstoffe hide-with-allergenes">Kennzeichnungen/Zusatzstoffe: 1,8</span> |  | ||||||
|                     <span class="zusatzsstoffe show-with-allergenes">Kennzeichnungen/Zusatzstoffe: 1,8<br>enthält Allergene: Sn,Se,Gl,GlW,GlG,ML,Lak</span> |  | ||||||
|                 </div> |  | ||||||
|                 <div class="col-md-4 col-sm-4 menu-price"> |  | ||||||
|                     <table class="table"> |  | ||||||
|                         <tbody> |  | ||||||
|                         <tr> |  | ||||||
|                             <td class="price-studierende">Studierende, Schüler</td> |  | ||||||
|                             <td>2,90 €</td> |  | ||||||
|                         </tr> |  | ||||||
|                         <tr> |  | ||||||
|                             <td class="price-mitarbeiter">Mitarbeiter</td> |  | ||||||
|                             <td>3,80 €</td> |  | ||||||
|                         </tr> |  | ||||||
|                         <tr> |  | ||||||
|                             <td class="price-gaeste">Gäste</td> |  | ||||||
|                             <td>5,10 €</td> |  | ||||||
|                         </tr> |  | ||||||
|                         </tbody> |  | ||||||
|                     </table> |  | ||||||
|                 </div> |  | ||||||
|             </div> |  | ||||||
|             <div class="row row-narrow row-table"> |  | ||||||
|                 <h4 class="col-md-3 col-sm-3 menu-header">Buffet</h4> |  | ||||||
|                 <div class="col-md-5 col-sm-5 menu-info bg-beige-hell "> |  | ||||||
|                     Grill und Salatbar |  | ||||||
|                     <br>Asiatisches aus dem Wok |  | ||||||
|                     <br>Fleisch- und Fischspezialitäten |  | ||||||
|                     <br>Pasta-, Reis und Kartoffelvariationen |  | ||||||
|                     <br>Gemüse |  | ||||||
|                 </div> |  | ||||||
|                 <div class="col-md-4 col-sm-4 menu-price"> |  | ||||||
|                     <table class="table"> |  | ||||||
|                         <tbody> |  | ||||||
|                         <tr> |  | ||||||
|                             <td class="price-studierende">Studierende, Schüler</td> |  | ||||||
|                             <td>0,85 €<span class="tara"> / 100g</span></td> |  | ||||||
|                         </tr> |  | ||||||
|                         <tr> |  | ||||||
|                             <td class="price-mitarbeiter">Mitarbeiter</td> |  | ||||||
|                             <td>1,00 €<span class="tara"> / 100g</span></td> |  | ||||||
|                         </tr> |  | ||||||
|                         <tr> |  | ||||||
|                             <td class="price-gaeste">Gäste</td> |  | ||||||
|                             <td>1,20 €<span class="tara"> / 100g</span></td> |  | ||||||
|                         </tr> |  | ||||||
|                         </tbody> |  | ||||||
|                     </table> |  | ||||||
|                 </div> |  | ||||||
|             </div> |  | ||||||
|         </div> |  | ||||||
|         <div id="tab-thu" class="menu-tagesplan r-tabs-panel r-tabs-state-default" style="display: none;"> |  | ||||||
|             <h3>Donnerstag 06.07.</h3> |  | ||||||
|             <div class="row row-narrow row-table"> |  | ||||||
|                 <h4 class="col-md-3 col-sm-3 menu-header">Essen 1</h4> |  | ||||||
|                 <div class="col-md-5 col-sm-5 menu-info bg-beige-hell vegetarisch"> |  | ||||||
|                     Farfalle tricolore |  | ||||||
|                     <br>Gorgonzola-Spinatsauce |  | ||||||
|                     <br>Endiviensalat |  | ||||||
|                     <span class="zusatzsstoffe show-with-allergenes">enthält Allergene: Gl,GlW,ML</span> |  | ||||||
|                 </div> |  | ||||||
|                 <div class="col-md-4 col-sm-4 menu-price"> |  | ||||||
|                     <table class="table"> |  | ||||||
|                         <tbody> |  | ||||||
|                         <tr> |  | ||||||
|                             <td class="price-studierende">Studierende, Schüler</td> |  | ||||||
|                             <td>2,90 €</td> |  | ||||||
|                         </tr> |  | ||||||
|                         <tr> |  | ||||||
|                             <td class="price-mitarbeiter">Mitarbeiter</td> |  | ||||||
|                             <td>3,80 €</td> |  | ||||||
|                         </tr> |  | ||||||
|                         <tr> |  | ||||||
|                             <td class="price-gaeste">Gäste</td> |  | ||||||
|                             <td>5,10 €</td> |  | ||||||
|                         </tr> |  | ||||||
|                         </tbody> |  | ||||||
|                     </table> |  | ||||||
|                 </div> |  | ||||||
|             </div> |  | ||||||
|             <div class="row row-narrow row-table"> |  | ||||||
|                 <h4 class="col-md-3 col-sm-3 menu-header">Essen 2</h4> |  | ||||||
|                 <div class="col-md-5 col-sm-5 menu-info bg-beige-hell "> |  | ||||||
|                     Paniertes Seelachsfilet MSC |  | ||||||
|                     <br>Remouladensauce |  | ||||||
|                     <br>Salzkartoffeln |  | ||||||
|                     <br>Brokkoligemüse |  | ||||||
|                     <span class="zusatzsstoffe hide-with-allergenes">Kennzeichnungen/Zusatzstoffe: 9,MSC</span> |  | ||||||
|                     <span class="zusatzsstoffe show-with-allergenes">Kennzeichnungen/Zusatzstoffe: 9,MSC<br>enthält Allergene: Ei,Sn,Fi,Gl,GlW,ML,Lak</span> |  | ||||||
|                 </div> |  | ||||||
|                 <div class="col-md-4 col-sm-4 menu-price"> |  | ||||||
|                     <table class="table"> |  | ||||||
|                         <tbody> |  | ||||||
|                         <tr> |  | ||||||
|                             <td class="price-studierende">Studierende, Schüler</td> |  | ||||||
|                             <td>2,90 €</td> |  | ||||||
|                         </tr> |  | ||||||
|                         <tr> |  | ||||||
|                             <td class="price-mitarbeiter">Mitarbeiter</td> |  | ||||||
|                             <td>3,80 €</td> |  | ||||||
|                         </tr> |  | ||||||
|                         <tr> |  | ||||||
|                             <td class="price-gaeste">Gäste</td> |  | ||||||
|                             <td>5,10 €</td> |  | ||||||
|                         </tr> |  | ||||||
|                         </tbody> |  | ||||||
|                     </table> |  | ||||||
|                 </div> |  | ||||||
|             </div> |  | ||||||
|             <div class="row row-narrow row-table"> |  | ||||||
|                 <h4 class="col-md-3 col-sm-3 menu-header">Buffet</h4> |  | ||||||
|                 <div class="col-md-5 col-sm-5 menu-info bg-beige-hell "> |  | ||||||
|                     Grill und Salatbar |  | ||||||
|                     <br>Asiatisches aus dem Wok |  | ||||||
|                     <br>Fleisch- und Fischspezialitäten |  | ||||||
|                     <br>Pasta-, Reis und Kartoffelvariationen |  | ||||||
|                     <br>Gemüse |  | ||||||
|                 </div> |  | ||||||
|                 <div class="col-md-4 col-sm-4 menu-price"> |  | ||||||
|                     <table class="table"> |  | ||||||
|                         <tbody> |  | ||||||
|                         <tr> |  | ||||||
|                             <td class="price-studierende">Studierende, Schüler</td> |  | ||||||
|                             <td>0,85 €<span class="tara"> / 100g</span></td> |  | ||||||
|                         </tr> |  | ||||||
|                         <tr> |  | ||||||
|                             <td class="price-mitarbeiter">Mitarbeiter</td> |  | ||||||
|                             <td>1,00 €<span class="tara"> / 100g</span></td> |  | ||||||
|                         </tr> |  | ||||||
|                         <tr> |  | ||||||
|                             <td class="price-gaeste">Gäste</td> |  | ||||||
|                             <td>1,20 €<span class="tara"> / 100g</span></td> |  | ||||||
|                         </tr> |  | ||||||
|                         </tbody> |  | ||||||
|                     </table> |  | ||||||
|                 </div> |  | ||||||
|             </div> |  | ||||||
|         </div> |  | ||||||
|         <div id="tab-fri" class="menu-tagesplan r-tabs-panel r-tabs-state-default" style="display: none;"> |  | ||||||
|             <h3>Freitag 07.07.</h3> |  | ||||||
|             <div class="row row-narrow row-table"> |  | ||||||
|                 <h4 class="col-md-3 col-sm-3 menu-header">Essen 1</h4> |  | ||||||
|                 <div class="col-md-5 col-sm-5 menu-info bg-beige-hell vegetarisch"> |  | ||||||
|                     Italienischer Nudelauflauf mit Gemüse |  | ||||||
|                     <br>Tomatenragout |  | ||||||
|                     <br>Blattsalat |  | ||||||
|                     <span class="zusatzsstoffe show-with-allergenes">enthält Allergene: Gl,GlW,ML,Lak</span> |  | ||||||
|                 </div> |  | ||||||
|                 <div class="col-md-4 col-sm-4 menu-price"> |  | ||||||
|                     <table class="table"> |  | ||||||
|                         <tbody> |  | ||||||
|                         <tr> |  | ||||||
|                             <td class="price-studierende">Studierende, Schüler</td> |  | ||||||
|                             <td>2,90 €</td> |  | ||||||
|                         </tr> |  | ||||||
|                         <tr> |  | ||||||
|                             <td class="price-mitarbeiter">Mitarbeiter</td> |  | ||||||
|                             <td>3,80 €</td> |  | ||||||
|                         </tr> |  | ||||||
|                         <tr> |  | ||||||
|                             <td class="price-gaeste">Gäste</td> |  | ||||||
|                             <td>5,10 €</td> |  | ||||||
|                         </tr> |  | ||||||
|                         </tbody> |  | ||||||
|                     </table> |  | ||||||
|                 </div> |  | ||||||
|             </div> |  | ||||||
|             <div class="row row-narrow row-table"> |  | ||||||
|                 <h4 class="col-md-3 col-sm-3 menu-header">Essen 2</h4> |  | ||||||
|                 <div class="col-md-5 col-sm-5 menu-info bg-beige-hell "> |  | ||||||
|                     Hähnchen-Saté-Spieß |  | ||||||
|                     <br>Erdnusssauce |  | ||||||
|                     <br>Langkornreis |  | ||||||
|                     <br>Karotten-Erbsengemüse |  | ||||||
|                     <span class="zusatzsstoffe show-with-allergenes">enthält Allergene: Er,So,Kr,Fi,We,Gl,GlW,ML,Lak</span> |  | ||||||
|                 </div> |  | ||||||
|                 <div class="col-md-4 col-sm-4 menu-price"> |  | ||||||
|                     <table class="table"> |  | ||||||
|                         <tbody> |  | ||||||
|                         <tr> |  | ||||||
|                             <td class="price-studierende">Studierende, Schüler</td> |  | ||||||
|                             <td>2,90 €</td> |  | ||||||
|                         </tr> |  | ||||||
|                         <tr> |  | ||||||
|                             <td class="price-mitarbeiter">Mitarbeiter</td> |  | ||||||
|                             <td>3,80 €</td> |  | ||||||
|                         </tr> |  | ||||||
|                         <tr> |  | ||||||
|                             <td class="price-gaeste">Gäste</td> |  | ||||||
|                             <td>5,10 €</td> |  | ||||||
|                         </tr> |  | ||||||
|                         </tbody> |  | ||||||
|                     </table> |  | ||||||
|                 </div> |  | ||||||
|             </div> |  | ||||||
|             <div class="row row-narrow row-table"> |  | ||||||
|                 <h4 class="col-md-3 col-sm-3 menu-header">Buffet</h4> |  | ||||||
|                 <div class="col-md-5 col-sm-5 menu-info bg-beige-hell "> |  | ||||||
|                     Grill und Salatbar |  | ||||||
|                     <br>Asiatisches aus dem Wok |  | ||||||
|                     <br>Fleisch- und Fischspezialitäten |  | ||||||
|                     <br>Pasta-, Reis und Kartoffelvariationen |  | ||||||
|                     <br>Gemüse |  | ||||||
|                 </div> |  | ||||||
|                 <div class="col-md-4 col-sm-4 menu-price"> |  | ||||||
|                     <table class="table"> |  | ||||||
|                         <tbody> |  | ||||||
|                         <tr> |  | ||||||
|                             <td class="price-studierende">Studierende, Schüler</td> |  | ||||||
|                             <td>0,85 €<span class="tara"> / 100g</span></td> |  | ||||||
|                         </tr> |  | ||||||
|                         <tr> |  | ||||||
|                             <td class="price-mitarbeiter">Mitarbeiter</td> |  | ||||||
|                             <td>1,00 €<span class="tara"> / 100g</span></td> |  | ||||||
|                         </tr> |  | ||||||
|                         <tr> |  | ||||||
|                             <td class="price-gaeste">Gäste</td> |  | ||||||
|                             <td>1,20 €<span class="tara"> / 100g</span></td> |  | ||||||
|                         </tr> |  | ||||||
|                         </tbody> |  | ||||||
|                     </table> |  | ||||||
|                 </div> |  | ||||||
|             </div> |  | ||||||
|         </div> |  | ||||||
|         <div id="tab-sat" class="menu-tagesplan r-tabs-panel r-tabs-state-default" style="display: none;"> |  | ||||||
|             <h3>Samstag 08.07.</h3> |  | ||||||
|             <div class="row row-narrow row-buffer row-table"> |  | ||||||
|                 <div class="col-md-1 bg-beige-hell zusatzangaben"> |  | ||||||
|                     <i class="glyphicons glyphicons-circle-info"></i> |  | ||||||
|                 </div> |  | ||||||
|                 <div class="col-md-11 border-beige-hell"> |  | ||||||
|                     <h4>heute keine Essensausgabe</h4> |  | ||||||
|                 </div> |  | ||||||
|             </div> |  | ||||||
|         </div> |  | ||||||
|     </div> |  | ||||||
| </div> |  | ||||||
| </div> |  | ||||||
|  |  | ||||||
|  |                      | ||||||
|  |  | ||||||
|  |                      | ||||||
|  |                             <div class="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-4 gap-y-30px md:gap-30px mb-30px"> | ||||||
|  |  | ||||||
|  |                                  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |                                  | ||||||
|  |                                     <div class="col-span-1 bg-lighter-cyan py-20px px-15px flex flex-col"> | ||||||
|  |                                         <div class="flex justify-between"> | ||||||
|  |                                             <h5>Essen 1</h5> | ||||||
|  |                                              | ||||||
|  |                                                     <img class="w-30px" src="/typo3conf/ext/speiseplan/Resources/Public/Images/vegetarisch.svg" width="27" height="27" alt="" /> | ||||||
|  |                                                  | ||||||
|  |                                         </div> | ||||||
|  |                                         <small class="extra-text mb-15px">auf Wunsch vegan<br>Gemüselasagne  oder<br>Cannelloni mit Gemüse und Käsekruste<br>Tomatensauce<br>Beilagensalat oder Regio-Apfel</small> | ||||||
|  |                                         <div class="border-t-1px border-light-cyan w-full pt-15px mt-15px flex justify-between"> | ||||||
|  |                                             <div id="accordion-collapse-1-1" class="w-full" data-accordion="collapse" data-active-classes="bg-primary-cyan text-white" data-inactive-classes="text-primary-cyan"> | ||||||
|  |                                                 <h4 id="accordion-collapse-heading-1-1"> | ||||||
|  |                                                     <button type="button" class="text-14px text-left extra-text text-primary-cyan flex items-center justify-between w-full" data-accordion-target="#accordion-collapse-body-1-1" aria-expanded="false" aria-controls="accordion-collapse-body--1"> | ||||||
|  |                                                         <span>Preise + Kennzeichnungen</span> | ||||||
|  |                                                         <img class="w-12px" src="/typo3conf/ext/speiseplan/Resources/Public/Images/plus-cyan.svg" width="12" height="12" alt="" /> | ||||||
|  |                                                     </button> | ||||||
|  |                                                 </h4> | ||||||
|  |  | ||||||
|  |                                                 <div id="accordion-collapse-body-1-1" class="hidden" aria-labelledby="accordion-collapse-heading-1-1"> | ||||||
|  |                                                     <dl class="text-14px mb-20px"> | ||||||
|  |                                                         <div class="flex items-end justify-between"> | ||||||
|  |                                                             <dt class="price-studierende">Studierende, Schüler</dt> | ||||||
|  |                                                             <dd>3,45 €</dd> | ||||||
|  |                                                         </div> | ||||||
|  |                                                         <div class="flex items-end justify-between"> | ||||||
|  |                                                             <dt class="price-mitarbeiter">Beschäftigte</dt> | ||||||
|  |                                                             <dd>4,75 €</dd> | ||||||
|  |                                                         </div> | ||||||
|  |                                                         <div class="flex items-end justify-between"> | ||||||
|  |                                                             <dt class="price-gaeste">Gäste</dt> | ||||||
|  |                                                             <dd>6,90 €</dd> | ||||||
|  |                                                         </div> | ||||||
|  |                                                     </dl> | ||||||
|  |                                                     <small class="zusatzsstoffe" x-show="!showAllergenes">Kennzeichnungen/Zusatzstoffe:<br>5</small> | ||||||
|  |                                                      | ||||||
|  |                                                             <small class="zusatzsstoffe" x-show="showAllergenes">Kennzeichnungen/Zusatzstoffe:<br> 5<br>enthält Allergene: ML,GlW,Gl,Ei,Se</small> | ||||||
|  |                                                          | ||||||
|  |                                                 </div> | ||||||
|  |                                             </div> | ||||||
|  |  | ||||||
|  |                                         </div> | ||||||
|  |                                     </div> | ||||||
|  |                                  | ||||||
|  |                                     <div class="col-span-1 bg-lighter-cyan py-20px px-15px flex flex-col"> | ||||||
|  |                                         <div class="flex justify-between"> | ||||||
|  |                                             <h5>Essen 2</h5> | ||||||
|  |                                              | ||||||
|  |                                         </div> | ||||||
|  |                                         <small class="extra-text mb-15px">Grönsaksbullar -Gemüsebällchen oder<br>Köttbullar<br>Pfeffer-Rahmsauce<br>Blumenkohl-Kartoffelpüree<br>Beilagensalat oder Regio-Apfel</small> | ||||||
|  |                                         <div class="border-t-1px border-light-cyan w-full pt-15px mt-15px flex justify-between"> | ||||||
|  |                                             <div id="accordion-collapse-1-2" class="w-full" data-accordion="collapse" data-active-classes="bg-primary-cyan text-white" data-inactive-classes="text-primary-cyan"> | ||||||
|  |                                                 <h4 id="accordion-collapse-heading-1-2"> | ||||||
|  |                                                     <button type="button" class="text-14px text-left extra-text text-primary-cyan flex items-center justify-between w-full" data-accordion-target="#accordion-collapse-body-1-2" aria-expanded="false" aria-controls="accordion-collapse-body--1"> | ||||||
|  |                                                         <span>Preise + Kennzeichnungen</span> | ||||||
|  |                                                         <img class="w-12px" src="/typo3conf/ext/speiseplan/Resources/Public/Images/plus-cyan.svg" width="12" height="12" alt="" /> | ||||||
|  |                                                     </button> | ||||||
|  |                                                 </h4> | ||||||
|  |  | ||||||
|  |                                                 <div id="accordion-collapse-body-1-2" class="hidden" aria-labelledby="accordion-collapse-heading-1-2"> | ||||||
|  |                                                     <dl class="text-14px mb-20px"> | ||||||
|  |                                                         <div class="flex items-end justify-between"> | ||||||
|  |                                                             <dt class="price-studierende">Studierende, Schüler</dt> | ||||||
|  |                                                             <dd>3,45 €</dd> | ||||||
|  |                                                         </div> | ||||||
|  |                                                         <div class="flex items-end justify-between"> | ||||||
|  |                                                             <dt class="price-mitarbeiter">Beschäftigte</dt> | ||||||
|  |                                                             <dd>4,75 €</dd> | ||||||
|  |                                                         </div> | ||||||
|  |                                                         <div class="flex items-end justify-between"> | ||||||
|  |                                                             <dt class="price-gaeste">Gäste</dt> | ||||||
|  |                                                             <dd>6,90 €</dd> | ||||||
|  |                                                         </div> | ||||||
|  |                                                     </dl> | ||||||
|  |                                                     <small class="zusatzsstoffe" x-show="!showAllergenes">Kennzeichnungen/Zusatzstoffe:<br>5,sch,ri,4</small> | ||||||
|  |                                                      | ||||||
|  |                                                             <small class="zusatzsstoffe" x-show="showAllergenes">Kennzeichnungen/Zusatzstoffe:<br> 5,sch,ri,4<br>enthält Allergene: ML,GlW,Gl,Ei</small> | ||||||
|  |                                                          | ||||||
|  |                                                 </div> | ||||||
|  |                                             </div> | ||||||
|  |  | ||||||
|  |                                         </div> | ||||||
|  |                                     </div> | ||||||
|  |                                  | ||||||
|  |                                  | ||||||
|  |                                     <div class="hidden xl:block xl:col-span-1 bg-lighter-green"></div> | ||||||
|  |                                  | ||||||
|  |                                     <div class="hidden xl:block xl:col-span-1 bg-lighter-green"></div> | ||||||
|  |                                  | ||||||
|  |                                     <div class="hidden xl:block xl:col-span-1 bg-lighter-green"></div> | ||||||
|  |                                  | ||||||
|  |                                     <div class="hidden xl:block xl:col-span-1 bg-lighter-green"></div> | ||||||
|  |                                  | ||||||
|  |                             </div> | ||||||
|  |                          | ||||||
|  |                 </div> | ||||||
|  |              | ||||||
|  |                 <div id="tab-tue" class="menu-tagesplan" role="tabpanel" aria-labelledby="label-tue" x-show="currentDay == 2"> | ||||||
|  |                     <h3>Dienstag 20.12.</h3> | ||||||
|  |  | ||||||
|  |                      | ||||||
|  |  | ||||||
|  |                      | ||||||
|  |                             <div class="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-4 gap-y-30px md:gap-30px mb-30px"> | ||||||
|  |  | ||||||
|  |                                  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |                                  | ||||||
|  |                                     <div class="col-span-1 bg-lighter-cyan py-20px px-15px flex flex-col"> | ||||||
|  |                                         <div class="flex justify-between"> | ||||||
|  |                                             <h5>Essen 1</h5> | ||||||
|  |                                              | ||||||
|  |                                                     <img class="w-30px" src="/typo3conf/ext/speiseplan/Resources/Public/Images/vegetarisch.svg" width="27" height="27" alt="" /> | ||||||
|  |                                                  | ||||||
|  |                                         </div> | ||||||
|  |                                         <small class="extra-text mb-15px">Brokkoli-Nussecken<br>Rotweinsauce<br>Kartoffel-Kürbispüree<br>Apfelrotkraut<br>Feldsalat</small> | ||||||
|  |                                         <div class="border-t-1px border-light-cyan w-full pt-15px mt-15px flex justify-between"> | ||||||
|  |                                             <div id="accordion-collapse-2-1" class="w-full" data-accordion="collapse" data-active-classes="bg-primary-cyan text-white" data-inactive-classes="text-primary-cyan"> | ||||||
|  |                                                 <h4 id="accordion-collapse-heading-2-1"> | ||||||
|  |                                                     <button type="button" class="text-14px text-left extra-text text-primary-cyan flex items-center justify-between w-full" data-accordion-target="#accordion-collapse-body-2-1" aria-expanded="false" aria-controls="accordion-collapse-body--2"> | ||||||
|  |                                                         <span>Preise + Kennzeichnungen</span> | ||||||
|  |                                                         <img class="w-12px" src="/typo3conf/ext/speiseplan/Resources/Public/Images/plus-cyan.svg" width="12" height="12" alt="" /> | ||||||
|  |                                                     </button> | ||||||
|  |                                                 </h4> | ||||||
|  |  | ||||||
|  |                                                 <div id="accordion-collapse-body-2-1" class="hidden" aria-labelledby="accordion-collapse-heading-2-1"> | ||||||
|  |                                                     <dl class="text-14px mb-20px"> | ||||||
|  |                                                         <div class="flex items-end justify-between"> | ||||||
|  |                                                             <dt class="price-studierende">Studierende, Schüler</dt> | ||||||
|  |                                                             <dd>3,45 €</dd> | ||||||
|  |                                                         </div> | ||||||
|  |                                                         <div class="flex items-end justify-between"> | ||||||
|  |                                                             <dt class="price-mitarbeiter">Beschäftigte</dt> | ||||||
|  |                                                             <dd>4,75 €</dd> | ||||||
|  |                                                         </div> | ||||||
|  |                                                         <div class="flex items-end justify-between"> | ||||||
|  |                                                             <dt class="price-gaeste">Gäste</dt> | ||||||
|  |                                                             <dd>6,90 €</dd> | ||||||
|  |                                                         </div> | ||||||
|  |                                                     </dl> | ||||||
|  |                                                     <small class="zusatzsstoffe" x-show="!showAllergenes">Kennzeichnungen/Zusatzstoffe:<br>5</small> | ||||||
|  |                                                      | ||||||
|  |                                                             <small class="zusatzsstoffe" x-show="showAllergenes">Kennzeichnungen/Zusatzstoffe:<br> 5<br>enthält Allergene: GlW,GlH,ML,NM,Gl,Sf,Se,Nu,NH</small> | ||||||
|  |                                                          | ||||||
|  |                                                 </div> | ||||||
|  |                                             </div> | ||||||
|  |  | ||||||
|  |                                         </div> | ||||||
|  |                                     </div> | ||||||
|  |                                  | ||||||
|  |                                     <div class="col-span-1 bg-lighter-cyan py-20px px-15px flex flex-col"> | ||||||
|  |                                         <div class="flex justify-between"> | ||||||
|  |                                             <h5>Essen 2</h5> | ||||||
|  |                                              | ||||||
|  |                                         </div> | ||||||
|  |                                         <small class="extra-text mb-15px">Weihnachtsessen<br>Hirschgulasch<br>Rotweinsauce<br>Bauernspätzle<br>Apfelrotkraut<br>Feldsalat</small> | ||||||
|  |                                         <div class="border-t-1px border-light-cyan w-full pt-15px mt-15px flex justify-between"> | ||||||
|  |                                             <div id="accordion-collapse-2-2" class="w-full" data-accordion="collapse" data-active-classes="bg-primary-cyan text-white" data-inactive-classes="text-primary-cyan"> | ||||||
|  |                                                 <h4 id="accordion-collapse-heading-2-2"> | ||||||
|  |                                                     <button type="button" class="text-14px text-left extra-text text-primary-cyan flex items-center justify-between w-full" data-accordion-target="#accordion-collapse-body-2-2" aria-expanded="false" aria-controls="accordion-collapse-body--2"> | ||||||
|  |                                                         <span>Preise + Kennzeichnungen</span> | ||||||
|  |                                                         <img class="w-12px" src="/typo3conf/ext/speiseplan/Resources/Public/Images/plus-cyan.svg" width="12" height="12" alt="" /> | ||||||
|  |                                                     </button> | ||||||
|  |                                                 </h4> | ||||||
|  |  | ||||||
|  |                                                 <div id="accordion-collapse-body-2-2" class="hidden" aria-labelledby="accordion-collapse-heading-2-2"> | ||||||
|  |                                                     <dl class="text-14px mb-20px"> | ||||||
|  |                                                         <div class="flex items-end justify-between"> | ||||||
|  |                                                             <dt class="price-studierende">Studierende, Schüler</dt> | ||||||
|  |                                                             <dd>3,45 €</dd> | ||||||
|  |                                                         </div> | ||||||
|  |                                                         <div class="flex items-end justify-between"> | ||||||
|  |                                                             <dt class="price-mitarbeiter">Beschäftigte</dt> | ||||||
|  |                                                             <dd>4,75 €</dd> | ||||||
|  |                                                         </div> | ||||||
|  |                                                         <div class="flex items-end justify-between"> | ||||||
|  |                                                             <dt class="price-gaeste">Gäste</dt> | ||||||
|  |                                                             <dd>6,90 €</dd> | ||||||
|  |                                                         </div> | ||||||
|  |                                                     </dl> | ||||||
|  |                                                     <small class="zusatzsstoffe" x-show="!showAllergenes">Kennzeichnungen/Zusatzstoffe:<br>5</small> | ||||||
|  |                                                      | ||||||
|  |                                                             <small class="zusatzsstoffe" x-show="showAllergenes">Kennzeichnungen/Zusatzstoffe:<br> 5<br>enthält Allergene: Ei,Gl,GlW,ML,Sf</small> | ||||||
|  |                                                          | ||||||
|  |                                                 </div> | ||||||
|  |                                             </div> | ||||||
|  |  | ||||||
|  |                                         </div> | ||||||
|  |                                     </div> | ||||||
|  |                                  | ||||||
|  |                                  | ||||||
|  |                                     <div class="hidden xl:block xl:col-span-1 bg-lighter-green"></div> | ||||||
|  |                                  | ||||||
|  |                                     <div class="hidden xl:block xl:col-span-1 bg-lighter-green"></div> | ||||||
|  |                                  | ||||||
|  |                                     <div class="hidden xl:block xl:col-span-1 bg-lighter-green"></div> | ||||||
|  |                                  | ||||||
|  |                                     <div class="hidden xl:block xl:col-span-1 bg-lighter-green"></div> | ||||||
|  |                                  | ||||||
|  |                             </div> | ||||||
|  |                          | ||||||
|  |                 </div> | ||||||
|  |              | ||||||
|  |                 <div id="tab-wed" class="menu-tagesplan" role="tabpanel" aria-labelledby="label-wed" x-show="currentDay == 3"> | ||||||
|  |                     <h3>Mittwoch 21.12.</h3> | ||||||
|  |  | ||||||
|  |                      | ||||||
|  |  | ||||||
|  |                      | ||||||
|  |                             <div class="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-4 gap-y-30px md:gap-30px mb-30px"> | ||||||
|  |  | ||||||
|  |                                  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |                                  | ||||||
|  |                                     <div class="col-span-1 bg-lighter-cyan py-20px px-15px flex flex-col"> | ||||||
|  |                                         <div class="flex justify-between"> | ||||||
|  |                                             <h5>Essen 1</h5> | ||||||
|  |                                              | ||||||
|  |                                                     <img class="w-30px" src="/typo3conf/ext/speiseplan/Resources/Public/Images/vegetarisch.svg" width="27" height="27" alt="" /> | ||||||
|  |                                                  | ||||||
|  |                                         </div> | ||||||
|  |                                         <small class="extra-text mb-15px">Gemüsenuggets<br>Mojo Dip<br>Gitterkartoffeln<br>Beilagensalat oder Regio-Apfel</small> | ||||||
|  |                                         <div class="border-t-1px border-light-cyan w-full pt-15px mt-15px flex justify-between"> | ||||||
|  |                                             <div id="accordion-collapse-3-1" class="w-full" data-accordion="collapse" data-active-classes="bg-primary-cyan text-white" data-inactive-classes="text-primary-cyan"> | ||||||
|  |                                                 <h4 id="accordion-collapse-heading-3-1"> | ||||||
|  |                                                     <button type="button" class="text-14px text-left extra-text text-primary-cyan flex items-center justify-between w-full" data-accordion-target="#accordion-collapse-body-3-1" aria-expanded="false" aria-controls="accordion-collapse-body--3"> | ||||||
|  |                                                         <span>Preise + Kennzeichnungen</span> | ||||||
|  |                                                         <img class="w-12px" src="/typo3conf/ext/speiseplan/Resources/Public/Images/plus-cyan.svg" width="12" height="12" alt="" /> | ||||||
|  |                                                     </button> | ||||||
|  |                                                 </h4> | ||||||
|  |  | ||||||
|  |                                                 <div id="accordion-collapse-body-3-1" class="hidden" aria-labelledby="accordion-collapse-heading-3-1"> | ||||||
|  |                                                     <dl class="text-14px mb-20px"> | ||||||
|  |                                                         <div class="flex items-end justify-between"> | ||||||
|  |                                                             <dt class="price-studierende">Studierende, Schüler</dt> | ||||||
|  |                                                             <dd>3,45 €</dd> | ||||||
|  |                                                         </div> | ||||||
|  |                                                         <div class="flex items-end justify-between"> | ||||||
|  |                                                             <dt class="price-mitarbeiter">Beschäftigte</dt> | ||||||
|  |                                                             <dd>4,75 €</dd> | ||||||
|  |                                                         </div> | ||||||
|  |                                                         <div class="flex items-end justify-between"> | ||||||
|  |                                                             <dt class="price-gaeste">Gäste</dt> | ||||||
|  |                                                             <dd>6,90 €</dd> | ||||||
|  |                                                         </div> | ||||||
|  |                                                     </dl> | ||||||
|  |                                                     <small class="zusatzsstoffe" x-show="!showAllergenes">Kennzeichnungen/Zusatzstoffe:<br>5</small> | ||||||
|  |                                                      | ||||||
|  |                                                             <small class="zusatzsstoffe" x-show="showAllergenes">Kennzeichnungen/Zusatzstoffe:<br> 5<br>enthält Allergene: GlW,GlG,Gl,ML</small> | ||||||
|  |                                                          | ||||||
|  |                                                 </div> | ||||||
|  |                                             </div> | ||||||
|  |  | ||||||
|  |                                         </div> | ||||||
|  |                                     </div> | ||||||
|  |                                  | ||||||
|  |                                     <div class="col-span-1 bg-lighter-cyan py-20px px-15px flex flex-col"> | ||||||
|  |                                         <div class="flex justify-between"> | ||||||
|  |                                             <h5>Essen 2</h5> | ||||||
|  |                                              | ||||||
|  |                                                     <img class="w-30px" src="/typo3conf/ext/speiseplan/Resources/Public/Images/vegan.svg" width="27" height="27" alt="" /> | ||||||
|  |                                                  | ||||||
|  |                                         </div> | ||||||
|  |                                         <small class="extra-text mb-15px">Gebratener Gemüsereis Nasi Goreng<br>Bio Tofu-Erdnusswürfel<br>Beilagensalat oder Regio-Apfel</small> | ||||||
|  |                                         <div class="border-t-1px border-light-cyan w-full pt-15px mt-15px flex justify-between"> | ||||||
|  |                                             <div id="accordion-collapse-3-2" class="w-full" data-accordion="collapse" data-active-classes="bg-primary-cyan text-white" data-inactive-classes="text-primary-cyan"> | ||||||
|  |                                                 <h4 id="accordion-collapse-heading-3-2"> | ||||||
|  |                                                     <button type="button" class="text-14px text-left extra-text text-primary-cyan flex items-center justify-between w-full" data-accordion-target="#accordion-collapse-body-3-2" aria-expanded="false" aria-controls="accordion-collapse-body--3"> | ||||||
|  |                                                         <span>Preise + Kennzeichnungen</span> | ||||||
|  |                                                         <img class="w-12px" src="/typo3conf/ext/speiseplan/Resources/Public/Images/plus-cyan.svg" width="12" height="12" alt="" /> | ||||||
|  |                                                     </button> | ||||||
|  |                                                 </h4> | ||||||
|  |  | ||||||
|  |                                                 <div id="accordion-collapse-body-3-2" class="hidden" aria-labelledby="accordion-collapse-heading-3-2"> | ||||||
|  |                                                     <dl class="text-14px mb-20px"> | ||||||
|  |                                                         <div class="flex items-end justify-between"> | ||||||
|  |                                                             <dt class="price-studierende">Studierende, Schüler</dt> | ||||||
|  |                                                             <dd>3,45 €</dd> | ||||||
|  |                                                         </div> | ||||||
|  |                                                         <div class="flex items-end justify-between"> | ||||||
|  |                                                             <dt class="price-mitarbeiter">Beschäftigte</dt> | ||||||
|  |                                                             <dd>4,75 €</dd> | ||||||
|  |                                                         </div> | ||||||
|  |                                                         <div class="flex items-end justify-between"> | ||||||
|  |                                                             <dt class="price-gaeste">Gäste</dt> | ||||||
|  |                                                             <dd>6,90 €</dd> | ||||||
|  |                                                         </div> | ||||||
|  |                                                     </dl> | ||||||
|  |                                                     <small class="zusatzsstoffe" x-show="!showAllergenes">Kennzeichnungen/Zusatzstoffe:<br>o</small> | ||||||
|  |                                                      | ||||||
|  |                                                             <small class="zusatzsstoffe" x-show="showAllergenes">Kennzeichnungen/Zusatzstoffe:<br> o<br>enthält Allergene: Gl,GlW,Se,Sf,So,Sn</small> | ||||||
|  |                                                          | ||||||
|  |                                                 </div> | ||||||
|  |                                             </div> | ||||||
|  |  | ||||||
|  |                                         </div> | ||||||
|  |                                     </div> | ||||||
|  |                                  | ||||||
|  |                                  | ||||||
|  |                                     <div class="hidden xl:block xl:col-span-1 bg-lighter-green"></div> | ||||||
|  |                                  | ||||||
|  |                                     <div class="hidden xl:block xl:col-span-1 bg-lighter-green"></div> | ||||||
|  |                                  | ||||||
|  |                                     <div class="hidden xl:block xl:col-span-1 bg-lighter-green"></div> | ||||||
|  |                                  | ||||||
|  |                                     <div class="hidden xl:block xl:col-span-1 bg-lighter-green"></div> | ||||||
|  |                                  | ||||||
|  |                             </div> | ||||||
|  |                          | ||||||
|  |                 </div> | ||||||
|  |              | ||||||
|  |                 <div id="tab-thu" class="menu-tagesplan" role="tabpanel" aria-labelledby="label-thu" x-show="currentDay == 4"> | ||||||
|  |                     <h3>Donnerstag 22.12.</h3> | ||||||
|  |  | ||||||
|  |                      | ||||||
|  |  | ||||||
|  |                      | ||||||
|  |                             <div class="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-4 gap-y-30px md:gap-30px mb-30px"> | ||||||
|  |  | ||||||
|  |                                  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |                                  | ||||||
|  |                                     <div class="col-span-1 bg-lighter-cyan py-20px px-15px flex flex-col"> | ||||||
|  |                                         <div class="flex justify-between"> | ||||||
|  |                                             <h5>Essen 1</h5> | ||||||
|  |                                              | ||||||
|  |                                                     <img class="w-30px" src="/typo3conf/ext/speiseplan/Resources/Public/Images/vegan.svg" width="27" height="27" alt="" /> | ||||||
|  |                                                  | ||||||
|  |                                         </div> | ||||||
|  |                                         <small class="extra-text mb-15px">Schupfnudel-Gemüsepfanne<br>Weiße Sauce<br>Beilagensalat oder Regio-Apfel</small> | ||||||
|  |                                         <div class="border-t-1px border-light-cyan w-full pt-15px mt-15px flex justify-between"> | ||||||
|  |                                             <div id="accordion-collapse-4-1" class="w-full" data-accordion="collapse" data-active-classes="bg-primary-cyan text-white" data-inactive-classes="text-primary-cyan"> | ||||||
|  |                                                 <h4 id="accordion-collapse-heading-4-1"> | ||||||
|  |                                                     <button type="button" class="text-14px text-left extra-text text-primary-cyan flex items-center justify-between w-full" data-accordion-target="#accordion-collapse-body-4-1" aria-expanded="false" aria-controls="accordion-collapse-body--4"> | ||||||
|  |                                                         <span>Preise + Kennzeichnungen</span> | ||||||
|  |                                                         <img class="w-12px" src="/typo3conf/ext/speiseplan/Resources/Public/Images/plus-cyan.svg" width="12" height="12" alt="" /> | ||||||
|  |                                                     </button> | ||||||
|  |                                                 </h4> | ||||||
|  |  | ||||||
|  |                                                 <div id="accordion-collapse-body-4-1" class="hidden" aria-labelledby="accordion-collapse-heading-4-1"> | ||||||
|  |                                                     <dl class="text-14px mb-20px"> | ||||||
|  |                                                         <div class="flex items-end justify-between"> | ||||||
|  |                                                             <dt class="price-studierende">Studierende, Schüler</dt> | ||||||
|  |                                                             <dd>3,45 €</dd> | ||||||
|  |                                                         </div> | ||||||
|  |                                                         <div class="flex items-end justify-between"> | ||||||
|  |                                                             <dt class="price-mitarbeiter">Beschäftigte</dt> | ||||||
|  |                                                             <dd>4,75 €</dd> | ||||||
|  |                                                         </div> | ||||||
|  |                                                         <div class="flex items-end justify-between"> | ||||||
|  |                                                             <dt class="price-gaeste">Gäste</dt> | ||||||
|  |                                                             <dd>6,90 €</dd> | ||||||
|  |                                                         </div> | ||||||
|  |                                                     </dl> | ||||||
|  |                                                      | ||||||
|  |                                                      | ||||||
|  |                                                             <small class="zusatzsstoffe" x-show="showAllergenes">enthält Allergene:<br> Gl,GlW,Se,So</small> | ||||||
|  |                                                          | ||||||
|  |                                                 </div> | ||||||
|  |                                             </div> | ||||||
|  |  | ||||||
|  |                                         </div> | ||||||
|  |                                     </div> | ||||||
|  |                                  | ||||||
|  |                                     <div class="col-span-1 bg-lighter-cyan py-20px px-15px flex flex-col"> | ||||||
|  |                                         <div class="flex justify-between"> | ||||||
|  |                                             <h5>Essen 2</h5> | ||||||
|  |                                              | ||||||
|  |                                         </div> | ||||||
|  |                                         <small class="extra-text mb-15px">Fischragout<br>Bandnudeln<br>Buntes Marktgemüse</small> | ||||||
|  |                                         <div class="border-t-1px border-light-cyan w-full pt-15px mt-15px flex justify-between"> | ||||||
|  |                                             <div id="accordion-collapse-4-2" class="w-full" data-accordion="collapse" data-active-classes="bg-primary-cyan text-white" data-inactive-classes="text-primary-cyan"> | ||||||
|  |                                                 <h4 id="accordion-collapse-heading-4-2"> | ||||||
|  |                                                     <button type="button" class="text-14px text-left extra-text text-primary-cyan flex items-center justify-between w-full" data-accordion-target="#accordion-collapse-body-4-2" aria-expanded="false" aria-controls="accordion-collapse-body--4"> | ||||||
|  |                                                         <span>Preise + Kennzeichnungen</span> | ||||||
|  |                                                         <img class="w-12px" src="/typo3conf/ext/speiseplan/Resources/Public/Images/plus-cyan.svg" width="12" height="12" alt="" /> | ||||||
|  |                                                     </button> | ||||||
|  |                                                 </h4> | ||||||
|  |  | ||||||
|  |                                                 <div id="accordion-collapse-body-4-2" class="hidden" aria-labelledby="accordion-collapse-heading-4-2"> | ||||||
|  |                                                     <dl class="text-14px mb-20px"> | ||||||
|  |                                                         <div class="flex items-end justify-between"> | ||||||
|  |                                                             <dt class="price-studierende">Studierende, Schüler</dt> | ||||||
|  |                                                             <dd>3,45 €</dd> | ||||||
|  |                                                         </div> | ||||||
|  |                                                         <div class="flex items-end justify-between"> | ||||||
|  |                                                             <dt class="price-mitarbeiter">Beschäftigte</dt> | ||||||
|  |                                                             <dd>4,75 €</dd> | ||||||
|  |                                                         </div> | ||||||
|  |                                                         <div class="flex items-end justify-between"> | ||||||
|  |                                                             <dt class="price-gaeste">Gäste</dt> | ||||||
|  |                                                             <dd>6,90 €</dd> | ||||||
|  |                                                         </div> | ||||||
|  |                                                     </dl> | ||||||
|  |                                                     <small class="zusatzsstoffe" x-show="!showAllergenes">Kennzeichnungen/Zusatzstoffe:<br>nF</small> | ||||||
|  |                                                      | ||||||
|  |                                                             <small class="zusatzsstoffe" x-show="showAllergenes">Kennzeichnungen/Zusatzstoffe:<br> nF<br>enthält Allergene: Se,ML,GlW,Gl,Ei,Fi</small> | ||||||
|  |                                                          | ||||||
|  |                                                 </div> | ||||||
|  |                                             </div> | ||||||
|  |  | ||||||
|  |                                         </div> | ||||||
|  |                                     </div> | ||||||
|  |                                  | ||||||
|  |                                  | ||||||
|  |                                     <div class="hidden xl:block xl:col-span-1 bg-lighter-green"></div> | ||||||
|  |                                  | ||||||
|  |                                     <div class="hidden xl:block xl:col-span-1 bg-lighter-green"></div> | ||||||
|  |                                  | ||||||
|  |                                     <div class="hidden xl:block xl:col-span-1 bg-lighter-green"></div> | ||||||
|  |                                  | ||||||
|  |                                     <div class="hidden xl:block xl:col-span-1 bg-lighter-green"></div> | ||||||
|  |                                  | ||||||
|  |                             </div> | ||||||
|  |                          | ||||||
|  |                 </div> | ||||||
|  |              | ||||||
|  |                 <div id="tab-fri" class="menu-tagesplan" role="tabpanel" aria-labelledby="label-fri" x-show="currentDay == 5"> | ||||||
|  |                     <h3>Freitag 23.12.</h3> | ||||||
|  |  | ||||||
|  |                      | ||||||
|  |  | ||||||
|  |                      | ||||||
|  |                             <div class="row row-narrow row-buffer row-table"> | ||||||
|  |                                 <div class="col-md-1 bg-beige-hell zusatzangaben"> | ||||||
|  |                                     <i class="glyphicons glyphicons-circle-info"></i> | ||||||
|  |                                 </div> | ||||||
|  |                                 <div class="col-md-11 border-beige-hell"> | ||||||
|  |                                     <h4>heute keine Essensausgabe</h4> | ||||||
|  |                                 </div> | ||||||
|  |                             </div> | ||||||
|  |                          | ||||||
|  |                 </div> | ||||||
|  |              | ||||||
|  |                 <div id="tab-sat" class="menu-tagesplan" role="tabpanel" aria-labelledby="label-sat" x-show="currentDay == 6"> | ||||||
|  |                     <h3>Samstag 24.12.</h3> | ||||||
|  |  | ||||||
|  |                      | ||||||
|  |  | ||||||
|  |                      | ||||||
|  |                             <div class="row row-narrow row-buffer row-table"> | ||||||
|  |                                 <div class="col-md-1 bg-beige-hell zusatzangaben"> | ||||||
|  |                                     <i class="glyphicons glyphicons-circle-info"></i> | ||||||
|  |                                 </div> | ||||||
|  |                                 <div class="col-md-11 border-beige-hell"> | ||||||
|  |                                     <h4>heute keine Essensausgabe</h4> | ||||||
|  |                                 </div> | ||||||
|  |                             </div> | ||||||
|  |                          | ||||||
|  |                 </div> | ||||||
|  |              | ||||||
|  |         </div> | ||||||
		Reference in New Issue
	
	Block a user