Compare commits
	
		
			94 Commits
		
	
	
		
			1.2.0
			...
			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 | 
							
								
								
									
										14
									
								
								.drone.yml
									
									
									
									
									
								
							
							
						
						
									
										14
									
								
								.drone.yml
									
									
									
									
									
								
							| @ -1,14 +0,0 @@ | ||||
| kind: pipeline | ||||
| name: default | ||||
|  | ||||
| steps: | ||||
| - name: test | ||||
|   image: gradle:jdk11 | ||||
|   commands: | ||||
|   - gradle test | ||||
|  | ||||
| - name: test-jdk8 | ||||
|   image: gradle:jdk8 | ||||
|   commands: | ||||
|   - gradle test | ||||
|  | ||||
							
								
								
									
										6
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										6
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @ -35,6 +35,7 @@ captures/ | ||||
|  | ||||
| # Intellij | ||||
| *.iml | ||||
| .idea/ | ||||
| .idea/workspace.xml | ||||
| .idea/tasks.xml | ||||
| .idea/gradle.xml | ||||
| @ -55,6 +56,9 @@ freeline.py | ||||
| freeline/ | ||||
| freeline_project_description.json | ||||
|  | ||||
| # KDE | ||||
| .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) | ||||
| # TheCitadelofRicks | ||||
|  | ||||
|  | ||||
							
								
								
									
										69
									
								
								build.gradle
									
									
									
									
									
								
							
							
						
						
									
										69
									
								
								build.gradle
									
									
									
									
									
								
							| @ -1,52 +1,45 @@ | ||||
|  | ||||
| buildscript { | ||||
|     ext.kotlin_version = '1.3.60' | ||||
|     ext.spring_boot_version = '2.1.10.RELEASE' | ||||
|  | ||||
|     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" | ||||
|     } | ||||
| plugins { | ||||
|     id 'org.jetbrains.kotlin.jvm' version '1.9.23' | ||||
|     id 'org.jetbrains.kotlin.plugin.spring' version '1.9.23' | ||||
|     id 'org.springframework.boot' version '3.2.4' | ||||
|     id 'io.spring.dependency-management' version '1.1.4' | ||||
| } | ||||
|  | ||||
| apply plugin: 'kotlin' | ||||
| apply plugin: 'kotlin-spring' | ||||
| apply plugin: 'org.springframework.boot' | ||||
| apply plugin: 'io.spring.dependency-management' | ||||
| group 'org.mosad' | ||||
| version '1.3.1' | ||||
|  | ||||
| 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 { | ||||
|     useJUnitPlatform() | ||||
|  | ||||
|     testLogging { | ||||
|         events "PASSED", "FAILED", "SKIPPED" | ||||
|         events 'PASSED', 'FAILED', 'SKIPPED' | ||||
|     } | ||||
| } | ||||
|  | ||||
| repositories { | ||||
|     jcenter() | ||||
| } | ||||
|  | ||||
| dependencies { | ||||
|     implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" | ||||
|     implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.2" | ||||
|     implementation "org.jsoup:jsoup:1.12.1" | ||||
|     implementation "org.springframework.boot:spring-boot-starter-web" | ||||
|     implementation "com.google.code.gson:gson:2.8.6" | ||||
|  | ||||
|     testImplementation("org.junit.jupiter:junit-jupiter:5.5.1") | ||||
| } | ||||
|  | ||||
| def jvmTargetVersion = "21" | ||||
| compileKotlin { | ||||
|     kotlinOptions.jvmTarget = "1.8" | ||||
|     kotlinOptions.jvmTarget = jvmTargetVersion | ||||
| } | ||||
| compileJava { | ||||
|     targetCompatibility = jvmTargetVersion | ||||
| } | ||||
| compileTestKotlin { | ||||
|     kotlinOptions.jvmTarget = "1.8" | ||||
|     kotlinOptions.jvmTarget = jvmTargetVersion | ||||
| } | ||||
| compileTestJava { | ||||
|     targetCompatibility = jvmTargetVersion | ||||
| } | ||||
|  | ||||
| group 'org.mosad' | ||||
| version '1.2.0' | ||||
|  | ||||
							
								
								
									
										
											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 | ||||
| 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 | ||||
| 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"); | ||||
| # 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 | ||||
| # 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" | ||||
| APP_BASE_NAME=`basename "$0"` | ||||
| # Resolve links: $0 may be a link | ||||
| 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. | ||||
| DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' | ||||
|  | ||||
| # Use the maximum available, or set MAX_FD != -1 to use that value. | ||||
| MAX_FD="maximum" | ||||
| MAX_FD=maximum | ||||
|  | ||||
| warn () { | ||||
|     echo "$*" | ||||
| } | ||||
| } >&2 | ||||
|  | ||||
| die () { | ||||
|     echo | ||||
|     echo "$*" | ||||
|     echo | ||||
|     exit 1 | ||||
| } | ||||
| } >&2 | ||||
|  | ||||
| # OS specific support (must be 'true' or 'false'). | ||||
| cygwin=false | ||||
| msys=false | ||||
| darwin=false | ||||
| nonstop=false | ||||
| case "`uname`" in | ||||
|   CYGWIN* ) | ||||
|     cygwin=true | ||||
|     ;; | ||||
|   Darwin* ) | ||||
|     darwin=true | ||||
|     ;; | ||||
|   MINGW* ) | ||||
|     msys=true | ||||
|     ;; | ||||
|   NONSTOP* ) | ||||
|     nonstop=true | ||||
|     ;; | ||||
| case "$( uname )" in                #( | ||||
|   CYGWIN* )         cygwin=true  ;; #( | ||||
|   Darwin* )         darwin=true  ;; #( | ||||
|   MSYS* | MINGW* )  msys=true    ;; #( | ||||
|   NONSTOP* )        nonstop=true ;; | ||||
| esac | ||||
|  | ||||
| CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar | ||||
|  | ||||
|  | ||||
| # Determine the Java command to use to start the JVM. | ||||
| if [ -n "$JAVA_HOME" ] ; then | ||||
|     if [ -x "$JAVA_HOME/jre/sh/java" ] ; then | ||||
|         # IBM's JDK on AIX uses strange locations for the executables | ||||
|         JAVACMD="$JAVA_HOME/jre/sh/java" | ||||
|         JAVACMD=$JAVA_HOME/jre/sh/java | ||||
|     else | ||||
|         JAVACMD="$JAVA_HOME/bin/java" | ||||
|         JAVACMD=$JAVA_HOME/bin/java | ||||
|     fi | ||||
|     if [ ! -x "$JAVACMD" ] ; then | ||||
|         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." | ||||
|     fi | ||||
| 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. | ||||
|  | ||||
| Please set the JAVA_HOME variable in your environment to match the | ||||
| @ -105,84 +140,105 @@ location of your Java installation." | ||||
| fi | ||||
|  | ||||
| # Increase the maximum file descriptors if we can. | ||||
| if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then | ||||
|     MAX_FD_LIMIT=`ulimit -H -n` | ||||
|     if [ $? -eq 0 ] ; then | ||||
|         if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then | ||||
|             MAX_FD="$MAX_FD_LIMIT" | ||||
|         fi | ||||
|         ulimit -n $MAX_FD | ||||
|         if [ $? -ne 0 ] ; then | ||||
|             warn "Could not set maximum file descriptor limit: $MAX_FD" | ||||
|         fi | ||||
|     else | ||||
|         warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" | ||||
|     fi | ||||
| fi | ||||
|  | ||||
| # 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" ;; | ||||
| if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then | ||||
|     case $MAX_FD in #( | ||||
|       max*) | ||||
|         # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. | ||||
|         # shellcheck disable=SC3045  | ||||
|         MAX_FD=$( ulimit -H -n ) || | ||||
|             warn "Could not query maximum file descriptor limit" | ||||
|     esac | ||||
|     case $MAX_FD in  #( | ||||
|       '' | soft) :;; #( | ||||
|       *) | ||||
|         # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. | ||||
|         # shellcheck disable=SC3045  | ||||
|         ulimit -n "$MAX_FD" || | ||||
|             warn "Could not set maximum file descriptor limit to $MAX_FD" | ||||
|     esac | ||||
| fi | ||||
|  | ||||
| # Escape application args | ||||
| save () { | ||||
|     for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done | ||||
|     echo " " | ||||
| } | ||||
| APP_ARGS=$(save "$@") | ||||
| # Collect all arguments for the java command, stacking in reverse order: | ||||
| #   * args from the command line | ||||
| #   * the main class name | ||||
| #   * -classpath | ||||
| #   * -D...appname settings | ||||
| #   * --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 | ||||
| eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" | ||||
| # For Cygwin or MSYS, switch paths to Windows format before running java | ||||
| 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 | ||||
| if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then | ||||
|   cd "$(dirname "$0")" | ||||
|     JAVACMD=$( cygpath --unix "$JAVACMD" ) | ||||
|  | ||||
|     # 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 | ||||
|  | ||||
| # 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" "$@" | ||||
|  | ||||
							
								
								
									
										38
									
								
								gradlew.bat
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										38
									
								
								gradlew.bat
									
									
									
									
										vendored
									
									
								
							| @ -14,7 +14,7 @@ | ||||
| @rem limitations under the License. | ||||
| @rem | ||||
|  | ||||
| @if "%DEBUG%" == "" @echo off | ||||
| @if "%DEBUG%"=="" @echo off | ||||
| @rem ########################################################################## | ||||
| @rem | ||||
| @rem  Gradle startup script for Windows | ||||
| @ -25,10 +25,14 @@ | ||||
| if "%OS%"=="Windows_NT" setlocal | ||||
|  | ||||
| set DIRNAME=%~dp0 | ||||
| if "%DIRNAME%" == "" set DIRNAME=. | ||||
| if "%DIRNAME%"=="" set DIRNAME=. | ||||
| @rem This is normally unused | ||||
| set APP_BASE_NAME=%~n0 | ||||
| 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. | ||||
| set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" | ||||
|  | ||||
| @ -37,7 +41,7 @@ if defined JAVA_HOME goto findJavaFromJavaHome | ||||
|  | ||||
| set JAVA_EXE=java.exe | ||||
| %JAVA_EXE% -version >NUL 2>&1 | ||||
| if "%ERRORLEVEL%" == "0" goto init | ||||
| if %ERRORLEVEL% equ 0 goto execute | ||||
|  | ||||
| echo. | ||||
| 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_EXE=%JAVA_HOME%/bin/java.exe | ||||
|  | ||||
| if exist "%JAVA_EXE%" goto init | ||||
| if exist "%JAVA_EXE%" goto execute | ||||
|  | ||||
| echo. | ||||
| echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% | ||||
| @ -61,38 +65,26 @@ echo location of your Java installation. | ||||
|  | ||||
| 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 | ||||
| @rem Setup the command line | ||||
|  | ||||
| set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar | ||||
|  | ||||
|  | ||||
| @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 | ||||
| @rem End local scope for the variables with windows NT shell | ||||
| if "%ERRORLEVEL%"=="0" goto mainEnd | ||||
| if %ERRORLEVEL% equ 0 goto mainEnd | ||||
|  | ||||
| :fail | ||||
| rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of | ||||
| rem the _cmd.exe /c_ return code! | ||||
| if  not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 | ||||
| exit /b 1 | ||||
| set EXIT_CODE=%ERRORLEVEL% | ||||
| if %EXIT_CODE% equ 0 set EXIT_CODE=1 | ||||
| if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% | ||||
| exit /b %EXIT_CODE% | ||||
|  | ||||
| :mainEnd | ||||
| if "%OS%"=="Windows_NT" endlocal | ||||
|  | ||||
| @ -1,7 +1,7 @@ | ||||
| /** | ||||
|  * TheCitadelofRicks | ||||
|  * | ||||
|  * Copyright 2019  <seil0@mosad.xyz> | ||||
|  * 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 | ||||
| @ -26,13 +26,17 @@ import org.mosad.thecitadelofricks.controller.CacheController | ||||
| import org.mosad.thecitadelofricks.controller.CacheController.Companion.courseList | ||||
| 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.mosad.thecitadelofricks.controller.StatusController.Companion.updateTotalRequests | ||||
| import org.slf4j.Logger | ||||
| import org.slf4j.LoggerFactory | ||||
| import org.springframework.web.bind.annotation.RequestMapping | ||||
| @ -40,6 +44,7 @@ import org.springframework.web.bind.annotation.RequestParam | ||||
| import org.springframework.web.bind.annotation.RestController | ||||
| import java.time.LocalDateTime | ||||
| import java.util.* | ||||
| import kotlin.collections.ArrayList | ||||
|  | ||||
| @RestController | ||||
| class APIController { | ||||
| @ -47,8 +52,8 @@ class APIController { | ||||
|     private val logger: Logger = LoggerFactory.getLogger(APIController::class.java) | ||||
|  | ||||
|     companion object { | ||||
|         const val apiVersion = "1.1.4" | ||||
|         const val softwareVersion = "1.2.0" | ||||
|         const val apiVersion = "1.4.0" | ||||
|         const val softwareVersion = "1.3.1" | ||||
|         val startTime = System.currentTimeMillis() / 1000 | ||||
|     } | ||||
|  | ||||
| @ -57,60 +62,69 @@ class APIController { | ||||
|         CacheController() | ||||
|     } | ||||
|  | ||||
|     // TODO remove this with API version 1.2.0 | ||||
|     @Deprecated("courses is replaced by courseList", replaceWith = ReplaceWith("courseList()")) | ||||
|     @RequestMapping("/courses") | ||||
|     fun courses(): CourseList { | ||||
|         return courseList() | ||||
|     } | ||||
|  | ||||
|     @RequestMapping("/courseList") | ||||
|     fun courseList(): CourseList { | ||||
|     fun courseList(): CoursesListRet { | ||||
|         logger.info("courseList request at ${LocalDateTime.now()}!") | ||||
|         updateTotalRequests() | ||||
|         return courseList | ||||
|         updateCourseListRequests() | ||||
|  | ||||
|         return CoursesListRet(courseList.meta, ArrayList(courseList.courses.values)) | ||||
|     } | ||||
|  | ||||
|     @RequestMapping("/mensamenu") | ||||
|     fun mensamenu(): MensaMenu { | ||||
|         logger.info("mensamenu request at ${LocalDateTime.now()}!") | ||||
|         updateTotalRequests() | ||||
|         updateMensaMenuRequests() | ||||
|         return mensaMenu | ||||
|     } | ||||
|  | ||||
|     @RequestMapping("/timetable") | ||||
|     fun timetable( | ||||
|         @RequestParam(value = "courseName", defaultValue = "AI4") courseName: String, | ||||
|         @RequestParam(value = "course", defaultValue = "AI4") courseName: String, | ||||
|         @RequestParam(value = "week", defaultValue = "0") week: Int | ||||
|     ): TimetableCourseWeek { | ||||
|         logger.info("timetable request at ${LocalDateTime.now()}!") | ||||
|         updateTotalRequests() | ||||
|         updateTimetableRequests(courseName) | ||||
|         return getTimetable(courseName, week) | ||||
|     } | ||||
|  | ||||
|     @RequestMapping("/lessonSubjectList") | ||||
|     @RequestMapping("/subjectList") | ||||
|     fun lessonSubjectList( | ||||
|         @RequestParam(value = "courseName", defaultValue = "AI4") courseName: String, | ||||
|         @RequestParam(value = "course", defaultValue = "AI4") courseName: String, | ||||
|         @RequestParam(value = "week", defaultValue = "0") week: Int | ||||
|     ): HashSet<String> { | ||||
|         logger.info("lessonSubjectList request at ${LocalDateTime.now()}!") | ||||
|         updateTotalRequests() | ||||
|         logger.info("subjectList request at ${LocalDateTime.now()}!") | ||||
|         updateTimetableRequests(courseName) | ||||
|         return getLessonSubjectList(courseName, week) | ||||
|     } | ||||
|  | ||||
|     @RequestMapping("/lessons") | ||||
|     fun lesson( | ||||
|         @RequestParam(value = "courseName", defaultValue = "AI4") courseName: String, | ||||
|         @RequestParam(value = "lessonSubject", defaultValue = "Mathematik 4") lessonSubject: String, | ||||
|         @RequestParam(value = "course", defaultValue = "AI4") courseName: String, | ||||
|         @RequestParam(value = "subject", defaultValue = "Mathematik 4") lessonSubject: String, | ||||
|         @RequestParam(value = "week", defaultValue = "0") week: Int | ||||
|     ): ArrayList<Lesson> { | ||||
|     ): ArrayList<LessonWithRoom> { | ||||
|         logger.info("lesson request at ${LocalDateTime.now()}!") | ||||
|         updateTotalRequests() | ||||
|         updateTimetableRequests(courseName) | ||||
|         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") | ||||
|     fun status(): Status { | ||||
|         logger.info("status request at ${LocalDateTime.now()}!") | ||||
| @ -123,4 +137,25 @@ class APIController { | ||||
|         return 200 | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Deprecated section | ||||
|      */ | ||||
|  | ||||
|     // TODO remove this with API version 2.0.0 | ||||
|     @Deprecated("courses is replaced by courseList", replaceWith = ReplaceWith("courseList()")) | ||||
|     @RequestMapping("/courses") | ||||
|     fun courses(): CoursesListRet { | ||||
|         return courseList() | ||||
|     } | ||||
|  | ||||
|     // TODO remove this with API version 2.0.0 | ||||
|     @Deprecated("the parameter courseName is deprecated please use course", ReplaceWith("timetable(courseName, week)")) | ||||
|     @RequestMapping("/timetable", params= ["courseName", "week"]) | ||||
|     fun timetableDep( | ||||
|         @RequestParam(value = "courseName", defaultValue = "AI4") courseName: String, | ||||
|         @RequestParam(value = "week", defaultValue = "0") week: Int | ||||
|     ): TimetableCourseWeek { | ||||
|        return timetable(courseName, week) | ||||
|     } | ||||
|  | ||||
| } | ||||
| @ -1,7 +1,7 @@ | ||||
| /** | ||||
|  * TheCitadelofRicks | ||||
|  * | ||||
|  * Copyright 2019  <seil0@mosad.xyz> | ||||
|  * 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 | ||||
|  | ||||
| @ -1,7 +1,7 @@ | ||||
| /** | ||||
|  * TheCitadelofRicks | ||||
|  * | ||||
|  * Copyright 2019  <seil0@mosad.xyz> | ||||
|  * 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 | ||||
| @ -24,13 +24,15 @@ package org.mosad.thecitadelofricks | ||||
|  | ||||
| import java.time.LocalDateTime | ||||
| import java.util.* | ||||
| import kotlin.collections.HashMap | ||||
|  | ||||
| // data classes for the course part | ||||
| 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 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 classes for the timetable part | ||||
| data class Lesson( | ||||
|  | ||||
| data class CalendarWeek(val week: Int, val year: Int) | ||||
|  | ||||
| data class LessonWithRoom( | ||||
|     val lessonID: String, | ||||
|     val lessonSubject: String, | ||||
|     val lessonTeacher: String, | ||||
| @ -52,28 +57,52 @@ data class Lesson( | ||||
|     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 = 0, val courseName: String = "", val weekIndex: Int = 0, val weekNumberYear: Int = 0, val link: String = "") | ||||
| data class TimetableWeek<Lesson>(val days: Array<TimetableDay<Lesson>> = Array(6) { TimetableDay() }) | ||||
|  | ||||
| data class TimetableCourseWeek(val meta: TimetableCourseMeta = TimetableCourseMeta(), var timetable: TimetableWeek = 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 class TimetableCounter(var courseName: String, var requests: Int) | ||||
|  | ||||
| data class Status( | ||||
|     val time: LocalDateTime, | ||||
|     val uptime: String, | ||||
|     val apiVersion: String, | ||||
|     val softwareVersion: String, | ||||
|     val requestCount: Int, | ||||
|     val totalRequests: Int, | ||||
|     val mensaMenuRequests: Int, | ||||
|     val timetableRequests: ArrayList<TimetableCounter>, | ||||
|     val courseListRequests: Int, | ||||
|     val timetableRequests: HashMap<String, Int>, | ||||
|     val timetableListSize: Int, | ||||
|     val coursesLastUpdate: Date, | ||||
|     val roomListRequests: Int, | ||||
|     val roomScheduleRequests: HashMap<String, Int>, | ||||
|     val roomScheduleListSize: Int, | ||||
|     val roomsLastUpdate: Date, | ||||
|     val mensaLastUpdate: Date, | ||||
|     val hsoResponseCode: Int, | ||||
|     val swfrResponseCode: Int | ||||
|  | ||||
| @ -1,7 +1,7 @@ | ||||
| /** | ||||
|  * TheCitadelofRicks | ||||
|  * | ||||
|  * Copyright 2019  <seil0@mosad.xyz> | ||||
|  * 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 | ||||
| @ -23,82 +23,102 @@ | ||||
| package org.mosad.thecitadelofricks.controller | ||||
|  | ||||
| import com.google.gson.Gson | ||||
| import kotlinx.coroutines.GlobalScope | ||||
| import kotlinx.coroutines.async | ||||
| import kotlinx.coroutines.launch | ||||
| import kotlinx.coroutines.runBlocking | ||||
| 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.TimetableParser | ||||
| import org.mosad.thecitadelofricks.hsoparser.RoomListParser | ||||
| import org.mosad.thecitadelofricks.hsoparser.RoomTimetableParser | ||||
| import org.slf4j.Logger | ||||
| import org.slf4j.LoggerFactory | ||||
| import java.io.BufferedWriter | ||||
| import java.io.File | ||||
| import java.io.FileWriter | ||||
| 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 { | ||||
|  | ||||
|     private val logger: Logger = LoggerFactory.getLogger(CacheController::class.java) | ||||
|  | ||||
|     init { | ||||
|         initUpdates() | ||||
|         scheduledUpdates() | ||||
|     } | ||||
|  | ||||
|     // cache objects | ||||
|     companion object{ | ||||
|     companion object { | ||||
|         private val logger: Logger = LoggerFactory.getLogger(CacheController::class.java) | ||||
|  | ||||
|         lateinit var courseList: CourseList | ||||
|         lateinit var mensaMenu: MensaMenu | ||||
|         var timetableList = ArrayList<TimetableCourseWeek>() // this list contains all timetables | ||||
|         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 cached, we need to make sure it's cached, otherwise download | ||||
|          * 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 = runBlocking { | ||||
|             val currentTime = System.currentTimeMillis() / 1000 | ||||
|             var timetable = TimetableWeek() | ||||
|             var weekNumberYear = 0 | ||||
|         fun getTimetable(courseName: String, weekIndex: Int): TimetableCourseWeek { | ||||
|  | ||||
|             // 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") | ||||
|             // 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 jobTimetable = GlobalScope.async { | ||||
|                         timetable = TimetableParser().getTimeTable(timetableLink) | ||||
|                         weekNumberYear = TimetableParser().getWeekNumberYear(timetableLink) | ||||
|                     } | ||||
|                 val timetableParser = | ||||
|                     CourseTimetableParser(htmlDoc = Jsoup.parse(instr!!, "UTF-8", "https://www.hs-offenburg.de/")) | ||||
|                 val timetableTest = timetableParser.parseTimeTable() | ||||
|  | ||||
|                     jobTimetable.await() | ||||
|  | ||||
|                     timetableList.add( | ||||
|                         TimetableCourseWeek(TimetableCourseMeta(currentTime, courseName, weekIndex, weekNumberYear, timetableLink), | ||||
|                             timetable | ||||
|                         ) | ||||
|                     ) | ||||
|  | ||||
|                     logger.info("added new timetable for $courseName, week $weekIndex") | ||||
|                 } | ||||
|                 return TimetableCourseWeek( | ||||
|                     TimetableCourseMeta( | ||||
|                         currentTime, | ||||
|                         courseName, | ||||
|                         weekIndex, | ||||
|                         weekNumberYear, | ||||
|                         year, | ||||
|                         timetableLink | ||||
|                     ), timetableTest ?: TimetableWeek() | ||||
|                 ) | ||||
|             } | ||||
|  | ||||
|             return@runBlocking timetableList.stream().filter { x -> x.meta.courseName == courseName && x.meta.weekIndex == weekIndex }.findAny().orElse(null) | ||||
|             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 | ||||
|          * 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 | ||||
| @ -116,170 +136,264 @@ class CacheController { | ||||
|         } | ||||
|  | ||||
|         /** | ||||
|          * get every explicit lesson in a week | ||||
|          * 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<Lesson> { | ||||
|             val lessonList = ArrayList<Lesson>() | ||||
|         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 { | ||||
|                 // TODO Java 11 | ||||
|                 //it.stream().filter { x -> x.lessonSubject.contains(lessonSubject) }.findAny().ifPresent { x -> println("${x.lessonSubject}, ${x.lessonTeacher}") } | ||||
|  | ||||
|                 it.forEach { lesson -> | ||||
|                     if(lesson.lessonSubject.contains(lessonSubject)) { | ||||
|                         lessonList.add(lesson) | ||||
|                     } | ||||
|                 } | ||||
|                 it.stream().filter { x -> x.lessonSubject.contains(lessonSubject) }.findAny().ifPresent { x -> lessonList.add(x) } | ||||
|             } | ||||
|  | ||||
|             return lessonList | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 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(StartupController.courseListURL)?.let { | ||||
|             courseList = CourseList( | ||||
|                 CourseMeta(System.currentTimeMillis() / 1000, it.size), it | ||||
|             ) | ||||
|         } | ||||
|         /** | ||||
|          * 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 | ||||
|  | ||||
|         logger.info("updated courses successful at ${Date(courseList.meta.updateTime * 1000)}") | ||||
|     } | ||||
|                 val roomScheduleParser = RoomTimetableParser(roomScheduleLink) | ||||
|                 val calendarWeek = roomScheduleParser.parseCalendarWeek() | ||||
|                 val roomSchedule = roomScheduleParser.parseTimeTable() | ||||
|  | ||||
|     /** | ||||
|      * 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(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 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 | ||||
|      * a FixedThreadPool is used to make parallel requests for faster updates | ||||
|      */ | ||||
|     private fun asyncUpdateTimetables() = GlobalScope.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 { | ||||
|                     timetableCourse.timetable = TimetableParser().getTimeTable(timetableCourse.meta.link) | ||||
|                     timetableCourse.meta.updateTime = System.currentTimeMillis() / 1000 | ||||
|  | ||||
|                     saveTimetableToCache(timetableCourse) // 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) { | ||||
|         println(timetable.timetable.toString()) | ||||
|  | ||||
|         val file = File(StartupController.dirTcorCache, "timetable-${timetable.meta.courseName}-${timetable.meta.weekIndex}.json") | ||||
|         val writer = BufferedWriter(FileWriter(file)) | ||||
|         writer.write(Gson().toJson(timetable)) | ||||
|         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 courses on startup | ||||
|         val jobCourseUpdate = GlobalScope.async { | ||||
|             CourseListParser().getCourseLinks(StartupController.courseListURL)?.let { | ||||
|                 courseList = CourseList( | ||||
|                     CourseMeta(System.currentTimeMillis() / 1000, it.size), it | ||||
|                 ) | ||||
|                 RoomScheduleWeekRet( | ||||
|                     RoomScheduleMeta( | ||||
|                         currentTime, | ||||
|                         roomName, | ||||
|                         weekIndex, | ||||
|                         calendarWeek?.week ?: 0, | ||||
|                         calendarWeek?.year ?: 0, | ||||
|                         roomScheduleLink | ||||
|                     ), roomSchedule ?: TimetableWeek() | ||||
|                 ).also { if (roomSchedule != null) roomScheduleList[key] = it } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         // get the current and next weeks mensa menus | ||||
|         val jobMensa = GlobalScope.async{ | ||||
|         // 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 | ||||
|                 ) | ||||
|                 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() | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         jobCourseUpdate.await() | ||||
|         jobMensa.await() | ||||
|         /** | ||||
|          * 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)) | ||||
|  | ||||
|         logger.info("init 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 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() | ||||
|         } | ||||
|  | ||||
|         // post to status.mosad.xyz every hour, if an API key is present | ||||
|         if (StartupController.cachetAPIKey != "0") { | ||||
|             Timer().scheduleAtFixedRate(initDelay1h, 3600000) { | ||||
|                 CachetAPIController.postTotalRequests() | ||||
|             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() | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|     } | ||||
| } | ||||
| @ -1,7 +1,7 @@ | ||||
| /** | ||||
|  * TheCitadelofRicks | ||||
|  * | ||||
|  * Copyright 2019  <seil0@mosad.xyz> | ||||
|  * 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 | ||||
| @ -22,14 +22,14 @@ | ||||
|  | ||||
| package org.mosad.thecitadelofricks.controller | ||||
|  | ||||
| import org.mosad.thecitadelofricks.controller.StatusController.Companion.getTotalRequests | ||||
| 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.URL | ||||
| import java.net.URI | ||||
|  | ||||
| class CachetAPIController { | ||||
|  | ||||
| @ -41,9 +41,9 @@ class CachetAPIController { | ||||
|  | ||||
|          fun postTotalRequests() { | ||||
|              try { | ||||
|                  val url = URL("${StartupController.cachetBaseURL}/api/v1/metrics/1/points") | ||||
|                  val jsonInputString = "{\"value\": ${getTotalRequests() -oldTotalRequests}, \"timestamp\": \"${(System.currentTimeMillis() / 1000)}\"}" | ||||
|                  oldTotalRequests = getTotalRequests() | ||||
|                  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" | ||||
|  | ||||
| @ -1,7 +1,7 @@ | ||||
| /** | ||||
|  * TheCitadelofRicks | ||||
|  * | ||||
|  * Copyright 2019  <seil0@mosad.xyz> | ||||
|  * 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 | ||||
| @ -35,8 +35,8 @@ class StartupController { | ||||
|     private val logger: Logger = LoggerFactory.getLogger(CacheController::class.java) | ||||
|  | ||||
|     companion object { | ||||
|         val userHome: String = System.getProperty("user.home") | ||||
|         val tcorHome = "$userHome/.tcor" | ||||
|         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") | ||||
| @ -44,7 +44,8 @@ class StartupController { | ||||
|         var cachetAPIKey = "0" | ||||
|         var cachetBaseURL = "https://status.mosad.xyz" | ||||
|         var courseListURL = "https://www.hs-offenburg.de/studium/vorlesungsplaene/" | ||||
|         var mensaMenuURL = "https://www.swfr.de/de/essen-trinken/speiseplaene/mensa-offenburg/" | ||||
|         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" | ||||
|     } | ||||
|  | ||||
| @ -66,7 +67,7 @@ class StartupController { | ||||
|             createConfig() | ||||
|         } | ||||
|  | ||||
|         // TODO read cached timetable files, as they are not initially cached | ||||
|         // read cached timetable files, as they are not initially cached | ||||
|         readCachedTimetables() | ||||
|     } | ||||
|  | ||||
| @ -77,29 +78,21 @@ class StartupController { | ||||
|         val properties = Properties() | ||||
|         properties.loadFromXML(FileInputStream(fileConfig)) | ||||
|  | ||||
|         cachetAPIKey = try { | ||||
|             properties.getProperty("cachetAPIKey") | ||||
|         } catch (ex: Exception) { | ||||
|             "0" | ||||
|         } | ||||
|         try { | ||||
|             cachetAPIKey = properties.getProperty("cachetAPIKey") | ||||
|         } catch (_: Exception) {} | ||||
|  | ||||
|         cachetBaseURL = try { | ||||
|             properties.getProperty("cachetBaseURL") | ||||
|         } catch (ex: Exception) { | ||||
|             "https://status.mosad.xyz" | ||||
|         } | ||||
|         try { | ||||
|             cachetBaseURL = properties.getProperty("cachetBaseURL") | ||||
|         } catch (_: Exception) {} | ||||
|  | ||||
|         mensaMenuURL = try { | ||||
|             properties.getProperty("mensaMenuURL") | ||||
|         } catch (ex: Exception) { | ||||
|             "https://www.swfr.de/de/essen-trinken/speiseplaene/mensa-offenburg/" | ||||
|         } | ||||
|         try { | ||||
|             mensaMenuURL = properties.getProperty("mensaMenuURL") | ||||
|         } catch (_: Exception) {} | ||||
|  | ||||
|         mensaName = try { | ||||
|             properties.getProperty("mensaName") | ||||
|         } catch (ex: Exception) { | ||||
|             "Offenburg" | ||||
|         } | ||||
|         try { | ||||
|             mensaName = properties.getProperty("mensaName") | ||||
|         } catch (_: Exception) {} | ||||
|  | ||||
|     } catch (ex: Exception) { | ||||
|         logger.error("error while loading config", ex) | ||||
| @ -111,10 +104,10 @@ class StartupController { | ||||
|     private fun createConfig() = try { | ||||
|         val properties = Properties() | ||||
|  | ||||
|         properties.setProperty("cachetAPIKey", "0") | ||||
|         properties.setProperty("cachetBaseURL", "https://status.mosad.xyz") | ||||
|         properties.setProperty("mensaMenuURL", "https://www.swfr.de/de/essen-trinken/speiseplaene/mensa-offenburg/") | ||||
|         properties.setProperty("mensaName", "Offenburg") | ||||
|         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") | ||||
| @ -129,18 +122,21 @@ class StartupController { | ||||
|     private fun readCachedTimetables() { | ||||
|         dirTcorCache.walkTopDown().forEach { | ||||
|             if (it.isFile && it.name.endsWith(".json")) { | ||||
|                 try { | ||||
|                     val fileReader = FileReader(it) | ||||
|                     val bufferedReader = BufferedReader(fileReader) | ||||
|                     val timetableObject = JsonParser.parseString(bufferedReader.readLine()).asJsonObject | ||||
|                 val fileReader = FileReader(it) | ||||
|                 val bufferedReader = BufferedReader(fileReader) | ||||
|  | ||||
|                     CacheController.timetableList.add(Gson().fromJson(timetableObject, TimetableCourseWeek().javaClass)) | ||||
|                     bufferedReader.close() | ||||
|                     fileReader.close() | ||||
|                 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() | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
| } | ||||
|  | ||||
| @ -1,7 +1,7 @@ | ||||
| /** | ||||
|  * TheCitadelofRicks | ||||
|  * | ||||
|  * Copyright 2019  <seil0@mosad.xyz> | ||||
|  * 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 | ||||
| @ -26,60 +26,59 @@ 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.mosad.thecitadelofricks.TimetableCounter | ||||
| 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.ArrayList | ||||
| import kotlin.collections.HashMap | ||||
|  | ||||
| class StatusController { | ||||
|  | ||||
|     companion object { | ||||
|         private val logger: Logger = LoggerFactory.getLogger(StatusController::class.java) | ||||
|  | ||||
|         private var totalRequests = 0 | ||||
|         private var mensaMenuRequests = 0 | ||||
|         private var timetableRequests = ArrayList<TimetableCounter>() | ||||
|         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 | ||||
|  | ||||
|         fun updateTotalRequests() { | ||||
|         /** | ||||
|          * if a mensamenu/courseList/timetable is requested update the specific and total request count | ||||
|          */ | ||||
|         fun updateMensaMenuRequests() { | ||||
|             mensaMenuRequests++ | ||||
|             totalRequests++ | ||||
|         } | ||||
|  | ||||
|         fun updateMensaMenuRequests() { | ||||
|             mensaMenuRequests++ | ||||
|         fun updateCourseListRequests() { | ||||
|             courseListRequests++ | ||||
|             totalRequests++ | ||||
|         } | ||||
|  | ||||
|         /** | ||||
|          * if a timetable is requested update the request counter | ||||
|          */ | ||||
|         fun updateTimetableRequests(courseName: String) { | ||||
|             when (timetableRequests.stream().filter { x -> x.courseName == courseName }.findAny().orElse(null)) { | ||||
|                 null -> timetableRequests.add(TimetableCounter(courseName, 0)) | ||||
|             } | ||||
|             timetableRequests.stream().filter { x -> x.courseName == courseName }.findFirst() | ||||
|                 .ifPresent { x -> x.requests++ } | ||||
|  | ||||
|             // TODO Java 11 | ||||
| //        timetableRequests.stream().filter { it.courseName == courseName }.findFirst().ifPresentOrElse({ | ||||
| //            it.requests++ | ||||
| //        }, { | ||||
| // | ||||
| //        }) | ||||
|             timetableRequests[courseName] = (timetableRequests[courseName] ?: 0) + 1 | ||||
|             totalRequests++ | ||||
|         } | ||||
|  | ||||
|         fun getTotalRequests(): Int { | ||||
|             return totalRequests | ||||
|         fun updateRoomListRequests() { | ||||
|             roomListRequests++ | ||||
|             totalRequests++ | ||||
|         } | ||||
|  | ||||
|         fun getMensaMenuRequests(): Int { | ||||
|             return mensaMenuRequests | ||||
|         } | ||||
|  | ||||
|         fun getTimetableRequests(): ArrayList<TimetableCounter> { | ||||
|             return timetableRequests | ||||
|         fun updateRoomScheduleRequests(roomName: String) { | ||||
|             roomScheduleRequests[roomName] = (roomScheduleRequests[roomName] ?: 0) + 1 | ||||
|             totalRequests++ | ||||
|         } | ||||
|  | ||||
|         fun getStatus(): Status { | ||||
| @ -92,8 +91,8 @@ class StatusController { | ||||
|             var swfrCode = 999 | ||||
|  | ||||
|             try { | ||||
|                 val hsoURL = URL("https://www.hs-offenburg.de/") | ||||
|                 val swfrURL = URL("https://www.swfr.de/") | ||||
|                 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" | ||||
| @ -113,11 +112,16 @@ class StatusController { | ||||
|                 "$days days, $hours:$minutes", | ||||
|                 apiVersion, | ||||
|                 softwareVersion, | ||||
|                 getTotalRequests(), | ||||
|                 getMensaMenuRequests(), | ||||
|                 getTimetableRequests(), | ||||
|                 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 | ||||
|  * | ||||
|  * Copyright 2019  <seil0@mosad.xyz> | ||||
|  * 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 | ||||
| @ -24,6 +24,7 @@ package org.mosad.thecitadelofricks.hsoparser | ||||
|  | ||||
| import org.jsoup.Jsoup | ||||
| import org.jsoup.nodes.Document | ||||
| import org.jsoup.parser.Parser | ||||
| import org.mosad.thecitadelofricks.Meal | ||||
| import org.mosad.thecitadelofricks.MensaWeek | ||||
| import org.slf4j.LoggerFactory | ||||
| @ -59,19 +60,24 @@ class MensaParser { | ||||
|         val mealWeekList = MensaWeek() | ||||
|  | ||||
|         try { | ||||
|             htmlDoc.select("#speiseplan-tabs").select("div.tab-content").select("div.menu-tagesplan") | ||||
|             htmlDoc.select("#tabsWeekdaysMenu").select("div.menu-tagesplan") | ||||
|                 .forEachIndexed { dayIndex, day -> | ||||
|                     val meals = mealWeekList.days[dayIndex].meals | ||||
|                     val strDay = day.select("h3").text() | ||||
|  | ||||
|                     day.select("div.menu-info").forEachIndexed { mealIndex, meal -> | ||||
|                         val heading = day.select("h4")[mealIndex].text() | ||||
|                         val parts = ArrayList(meal.html().substringBefore("<br>\n").replace("\n", "").split("<br>")) | ||||
|                         val additives = meal.select("span.show-with-allergenes").text() | ||||
|                     day.select("div.menu-tagesplan > div.grid").first()?.select("div.flex-col")?.forEachIndexed { _, meal -> | ||||
|                         val heading = meal.select("h5").text() | ||||
|                         val parts = ArrayList(meal.select("small.extra-text").html().split("<br>").map { Parser.unescapeEntities(it, true).trim() }) | ||||
|                         val additives = meal.select("small.zusatzsstoffe[x-show=showAllergenes]").text() | ||||
|                         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) { | ||||
|             logger.error("error while parsing the html file", pex) | ||||
| @ -87,8 +93,7 @@ class MensaParser { | ||||
|      */ | ||||
|     fun getMenuLinkNextWeek(mensaMenuURL: String): String { | ||||
|         val menuHTML = Jsoup.connect(mensaMenuURL).get() | ||||
|  | ||||
|         return "https://www.swfr.de" + menuHTML.select("#speiseplan-tabs").select("a.next-week").attr("href") | ||||
|         return "https://www.swfr.de" + menuHTML.select("div.section-mensa").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 | ||||
|  * | ||||
|  * Copyright 2019  <seil0@mosad.xyz> | ||||
|  * 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 | ||||
| @ -22,38 +22,58 @@ | ||||
|  | ||||
| package org.mosad.thecitadelofricks.hsoparser | ||||
|  | ||||
| import kotlinx.coroutines.runBlocking | ||||
| import kotlinx.coroutines.sync.Semaphore | ||||
| import org.jsoup.Jsoup | ||||
| 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.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 val days = arrayOf("Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday") | ||||
|  | ||||
|     /** | ||||
|      * get the timetable from the given url | ||||
|      * 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 | ||||
|      * @param timetableURL the URL of the timetable you want to get | ||||
|      */ | ||||
|     fun getTimeTable(timetableURL: String): TimetableWeek { | ||||
|         return try { | ||||
|             parseTimeTable(Jsoup.connect(timetableURL).get()) | ||||
|         } catch (gex: Exception) { | ||||
|             logger.error("general TimetableParser error", gex) | ||||
|             TimetableWeek() | ||||
|     abstract fun constructLesson(lessonID: String, lessonSubject: String, lessonTeacher: String, value4: String, lessonRemark: String): Lesson | ||||
|     abstract val value4Class: String | ||||
|  | ||||
|     companion object { | ||||
|         val semaphore = Semaphore(3, 0) | ||||
|     } | ||||
|  | ||||
|     private val htmlDoc: Document? = htmlDoc ?: timetableURL?.let { | ||||
|         runBlocking { | ||||
|             try { | ||||
|                 // Only allow sending a limited amount of requests at the same time | ||||
|                 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() | ||||
|         val rows = htmlDoc.select("table.timetable").select("tr[scope=\"row\"]") | ||||
|     /** | ||||
|      * parse the timetable from the previously given url | ||||
|      * 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 sRow = -1 | ||||
|         var sLesson = Lesson("", "", "", "", "") | ||||
|         var sLesson = constructLesson("", "", "", "", "") | ||||
|  | ||||
|         // get each row with index, reflects 1 timeslot per day | ||||
|         for ((rowIndex, row) in rows.withIndex()) { | ||||
| @ -61,7 +81,7 @@ class TimetableParser { | ||||
|             var lessonIndexDay = 0 // the index of the lesson per timeslot | ||||
|  | ||||
|             // 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 ((sDay > -1 && sRow > -1) && (sDay == day && ((sRow + 1) == rowIndex))) { | ||||
| @ -70,11 +90,11 @@ class TimetableParser { | ||||
|  | ||||
|                     // adjust the following slot | ||||
|                     sDay++ | ||||
|                     sLesson = Lesson( | ||||
|                     sLesson = constructLesson( | ||||
|                         "$day.$rowIndex.$lessonIndexDay", | ||||
|                         element.select("div.lesson-subject").text(), | ||||
|                         element.select("div.lesson-teacher").text(), | ||||
|                         element.select("div.lesson-room").text(), | ||||
|                         element.select("div.$value4Class").text(), | ||||
|                         element.select("div.lesson-remark").text() | ||||
|                     ) | ||||
|  | ||||
| @ -85,11 +105,11 @@ class TimetableParser { | ||||
|  | ||||
|                 } else { | ||||
|                     timetableWeek.days[day].timeslots[rowIndex].add( | ||||
|                         Lesson( | ||||
|                         constructLesson( | ||||
|                             "$day.$rowIndex.$lessonIndexDay", | ||||
|                             element.select("div.lesson-subject").text(), | ||||
|                             element.select("div.lesson-teacher").text(), | ||||
|                             element.select("div.lesson-room").text(), | ||||
|                             element.select("div.$value4Class").text(), | ||||
|                             element.select("div.lesson-remark").text() | ||||
|                         ) | ||||
|                     ) | ||||
| @ -104,8 +124,7 @@ class TimetableParser { | ||||
|  | ||||
|                 lessonIndexDay++ | ||||
|  | ||||
|                 if (element.hasClass("lastcol")) | ||||
|                 { | ||||
|                 if (element.hasClass("lastcol")) { | ||||
|                     day++ | ||||
|                     lessonIndexDay = 0 | ||||
|                 } | ||||
| @ -117,21 +136,13 @@ class TimetableParser { | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * get the week number of the year for the timetable | ||||
|      * @param timetableURL the URL of the timetable you want to get | ||||
|      * parse the calendar week and the associated year for the timetable | ||||
|      */ | ||||
|     fun getWeekNumberYear(timetableURL: String): Int { | ||||
|         return try { | ||||
|             parseWeekNumberYear(Jsoup.connect(timetableURL).get()) | ||||
|         } catch (gex: Exception) { | ||||
|             logger.error("general TimetableParser error", gex) | ||||
|             0 | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     fun parseWeekNumberYear(htmlDoc: Document): Int { | ||||
|         return htmlDoc.select("h1.timetable-caption").text().substringAfter("- ") | ||||
|             .substringBefore(".").replace(" ", "").toInt() | ||||
|     fun parseCalendarWeek(): CalendarWeek? = htmlDoc?.let { | ||||
|         val dateStr = it.select("h1.timetable-caption").text().substringAfter("- ") | ||||
|         val week = dateStr.substringBefore(".").replace(" ", "").toInt() | ||||
|         val year = dateStr.substringAfter("Woche ").replace(" ", "").toInt() | ||||
|         CalendarWeek(week, year) | ||||
|     } | ||||
|  | ||||
|     @Suppress("unused") | ||||
| @ -139,7 +150,7 @@ class TimetableParser { | ||||
|      * print a timetable | ||||
|      * @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, ' ') + " | ") | ||||
|         println() | ||||
|         for (j in 0..5) print("-".padEnd(76 + (j.toFloat().div(j).toInt()), '-') + "+") | ||||
| @ -181,4 +192,19 @@ class TimetableParser { | ||||
|  | ||||
|         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) | ||||
| server.address=127.0.0.1 | ||||
| server.address=0.0.0.0 | ||||
| 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 | ||||
|  * | ||||
|  * Copyright 2019  <seil0@mosad.xyz> | ||||
|  * 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 | ||||
| @ -28,20 +28,16 @@ import org.junit.jupiter.api.Test | ||||
| import java.io.File | ||||
|  | ||||
| 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 | ||||
|     fun parseMensaMenuNormalWeek() { | ||||
|         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 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( | ||||
|             "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() | ||||
|         ) | ||||
|  | ||||
|         Assertions.assertEquals(expectedOutput, mensaWeek.toString()) | ||||
|     } | ||||
|  | ||||
|     @Test | ||||
| @ -49,11 +45,9 @@ internal class MensaParserTest { | ||||
|         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 mensaWeek = MensaParser().parseMensaMenu(htmlDoc) | ||||
|         val expectedOutput = MensaParserTest::class.java.getResource("/expected/Mensa_empty-week.txt").readText() | ||||
|  | ||||
|         Assertions.assertEquals( | ||||
|             "MensaWeek(days=[Meals(meals=[]), Meals(meals=[]), Meals(meals=[]), Meals(meals=[]), Meals(meals=[]), Meals(meals=[]), Meals(meals=[])])", | ||||
|             mensaWeek.toString() | ||||
|         ) | ||||
|         Assertions.assertEquals(expectedOutput, mensaWeek.toString()) | ||||
|     } | ||||
|  | ||||
|     // 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 | ||||
|         Assertions.assertNotNull(urlNextWeek) | ||||
|     } | ||||
| } | ||||
| } | ||||
|  | ||||
| @ -1,7 +1,7 @@ | ||||
| /** | ||||
|  * TheCitadelofRicks | ||||
|  * | ||||
|  * Copyright 2019  <seil0@mosad.xyz> | ||||
|  * 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 | ||||
| @ -25,6 +25,7 @@ package org.mosad.thecitadelofricks.hsoparser | ||||
| import org.jsoup.Jsoup | ||||
| import org.junit.jupiter.api.Assertions | ||||
| import org.junit.jupiter.api.Test | ||||
| import org.mosad.thecitadelofricks.CalendarWeek | ||||
| import java.io.File | ||||
|  | ||||
| class TimetableParserTest { | ||||
| @ -32,9 +33,9 @@ class TimetableParserTest { | ||||
|     @Test | ||||
|     fun parseTimetableNormalWeek() { | ||||
|         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 actualTimetable = TimetableParser().parseTimeTable(htmlDoc).toString().trim() | ||||
|         val expectedTimetable = File(TimetableParserTest::class.java.getResource("/expected/Timetable_normal-week_expected.txt").toURI()).readText().trim() | ||||
|         val htmlDoc = Jsoup.parse(htmlFile, "UTF-8", "https://www.hs-offenburg.de/") | ||||
|         val actualTimetable = CourseTimetableParser(htmlDoc = htmlDoc).parseTimeTable().toString().trim() | ||||
|         val expectedTimetable = TimetableParserTest::class.java.getResource("/expected/Timetable_normal-week.txt").readText().trim() | ||||
|  | ||||
|         Assertions.assertEquals(expectedTimetable, actualTimetable) | ||||
|     } | ||||
| @ -42,19 +43,19 @@ class TimetableParserTest { | ||||
|     @Test | ||||
|     fun parseTimetableEmptyWeek() { | ||||
|         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 actualTimetable = TimetableParser().parseTimeTable(htmlDoc).toString().trim() | ||||
|         val expectedTimetable = File(TimetableParserTest::class.java.getResource("/expected/Timetable_empty-week_expected.txt").toURI()).readText().trim() | ||||
|         val htmlDoc = Jsoup.parse(htmlFile, "UTF-8", "https://www.hs-offenburg.de/") | ||||
|         val actualTimetable = CourseTimetableParser(htmlDoc = htmlDoc).parseTimeTable().toString().trim() | ||||
|         val expectedTimetable = TimetableParserTest::class.java.getResource("/expected/Timetable_empty-week.txt").readText().trim() | ||||
|  | ||||
|         Assertions.assertEquals(expectedTimetable, actualTimetable) | ||||
|     } | ||||
|  | ||||
|     @Test | ||||
|     fun parseWeekNumberYear() { | ||||
|     fun parseCalendarWeek() { | ||||
|         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 actualWeekNumberYear = TimetableParser().parseWeekNumberYear(htmlDoc) | ||||
|         val htmlDoc = Jsoup.parse(htmlFile, "UTF-8", "https://www.hs-offenburg.de/") | ||||
|         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="tab-menu-container" class="row"> | ||||
|         <div class="col-md-1 col-xs-6"></div> | ||||
|         <div class="col-md-10"> | ||||
|             <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 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 id="tabsWeekdaysMenu"> | ||||
|              | ||||
|                 <div id="tab-mon" class="menu-tagesplan" role="tabpanel" aria-labelledby="label-mon" x-show="currentDay == 1"> | ||||
|                     <h3>Montag 26.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 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 id="tab-tue" class="menu-tagesplan tab-pane fade"> | ||||
|             <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 id="tab-wed" class="menu-tagesplan" role="tabpanel" aria-labelledby="label-wed" x-show="currentDay == 3"> | ||||
|                     <h3>Mittwoch 28.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 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 id="tab-wed" class="menu-tagesplan tab-pane fade"> | ||||
|             <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 id="tab-fri" class="menu-tagesplan" role="tabpanel" aria-labelledby="label-fri" x-show="currentDay == 5"> | ||||
|                     <h3>Freitag 30.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 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 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> | ||||
|              | ||||
|         </div> | ||||
| @ -1,518 +1,467 @@ | ||||
| <div id="speiseplan-tabs"> | ||||
|     <div id="tab-menu-container" class="row"> | ||||
|         <div class="col-md-1 col-xs-6"></div> | ||||
|         <div class="col-md-10"> | ||||
|             <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 id="tabsWeekdaysMenu"> | ||||
|              | ||||
|                 <div id="tab-mon" class="menu-tagesplan" role="tabpanel" aria-labelledby="label-mon" x-show="currentDay == 1"> | ||||
|                     <h3>Montag 19.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">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