17 Commits
1.1.3 ... 1.1.6

Author SHA1 Message Date
db57059727 version 1.1.6
All checks were successful
continuous-integration/drone/push Build is passing
* API version 1.1.4
* added /health, returns 200 (OK, for status checks)
* moved status code to a separate class
* added status.mosad.xyz reporting
2019-10-21 18:46:21 +02:00
132cf2df0e reworked the CacheController Class
All checks were successful
continuous-integration/drone/push Build is passing
2019-10-20 11:52:44 +02:00
283cc96e48 fixed drone formatting
All checks were successful
continuous-integration/drone/push Build is passing
2019-10-06 12:12:56 +02:00
238947a894 added java 8 support again
* added drone testing for java 8
2019-10-06 12:11:02 +02:00
26f1834183 try running ci with jdk11
All checks were successful
continuous-integration/drone/push Build is passing
2019-10-05 17:21:35 +02:00
6a687eb297 added more status metrics, api version 1.1.3 (/lesson -> /lessons)
Some checks failed
continuous-integration/drone/push Build is failing
* updated spring boot 2.1.8 -> 2.1.9
* closes #7
2019-10-05 17:17:26 +02:00
3be98f6129 added junit test for TimetableParser
All checks were successful
continuous-integration/drone/push Build is passing
* this also is release 1.1.4 (api version 1.1.2)
* closes issue #5
2019-09-14 00:43:59 +02:00
7d1f825057 more gradle wrapper stuff
All checks were successful
continuous-integration/drone/push Build is passing
2019-09-07 20:59:18 +02:00
c4b3a09d23 updated gradle wrapper to gradle 5.6.2
All checks were successful
continuous-integration/drone/push Build is passing
2019-09-07 20:56:58 +02:00
715af9987b updated some libs
All checks were successful
continuous-integration/drone/push Build is passing
* spring boot2.1.7 -> 2.1.8
* coroutines-core 1.2.2 -> 1.3.2
* print test results (passed, failed, skipped)
2019-09-07 20:44:44 +02:00
9797e23efb Update 'README.md'
All checks were successful
continuous-integration/drone/push Build is passing
2019-09-07 19:12:23 +02:00
c7182bc095 added drone ci
All checks were successful
continuous-integration/drone/push Build is passing
2019-09-07 19:08:08 +02:00
5e220225f6 parse the week number of the year too 2019-09-06 23:10:35 +02:00
ea6be1db33 update to kotlin 1.3.50 2019-08-25 22:46:14 +02:00
a0fceccc2f added junit test for MensaParser
* updated kotlin 1.3.31 -> 1.3.41
* updated spring boot 2.1.5 -> 2.1.7
* updated coroutines 1.2.1 -> 1.2.2
* reworked MensaParser to make it testable
2019-08-16 13:59:44 +02:00
e89201c951 added more metrics to the status request 2019-06-03 12:20:01 +02:00
a6bb0a7afe an hour needs to bee an hour 2019-06-02 19:04:20 +02:00
23 changed files with 4340 additions and 306 deletions

14
.drone.yml Normal file
View File

@ -0,0 +1,14 @@
kind: pipeline
name: default
steps:
- name: test
image: gradle:jdk11
commands:
- gradle test
- name: test-jdk8
image: gradle:jdk8
commands:
- gradle test

View File

@ -1,3 +1,5 @@
[![Build Status](https://drone.mosad.xyz/api/badges/Seil0/TheCitadelofRicks/status.svg)](https://drone.mosad.xyz/Seil0/TheCitadelofRicks)
[![License: GPL v3](https://img.shields.io/badge/License-GPL%20v3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0)
# TheCitadelofRicks
The API backend for [ProjectLaogai](https://git.mosad.xyz/Seil0/ProjectLaogai).

View File

@ -1,7 +1,7 @@
buildscript {
ext.kotlin_version = '1.3.31'
ext.spring_boot_version = '2.1.5.RELEASE'
ext.kotlin_version = '1.3.50'
ext.spring_boot_version = '2.1.9.RELEASE'
repositories {
jcenter()
@ -19,15 +19,25 @@ apply plugin: 'kotlin-spring'
apply plugin: 'org.springframework.boot'
apply plugin: 'io.spring.dependency-management'
test {
useJUnitPlatform()
testLogging {
events "PASSED", "FAILED", "SKIPPED"
}
}
repositories {
jcenter()
}
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.2.1"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.1"
implementation 'org.jsoup:jsoup:1.12.1'
implementation 'org.springframework.boot:spring-boot-starter-web'
testImplementation("org.junit.jupiter:junit-jupiter:5.5.1")
}
compileKotlin {
@ -38,4 +48,4 @@ compileTestKotlin {
}
group 'org.mosad'
version '1.1.3'
version '1.1.6'

Binary file not shown.

View File

@ -1,6 +1,5 @@
#Mon Mar 11 19:03:50 CET 2019
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.2-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-5.4.1-all.zip

22
gradlew vendored
View File

@ -1,5 +1,21 @@
#!/usr/bin/env sh
#
# Copyright 2015 the original author or authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
##############################################################################
##
## Gradle start up script for UN*X
@ -28,7 +44,7 @@ APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS=""
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
@ -109,8 +125,8 @@ if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin, switch paths to Windows format before running java
if $cygwin ; then
# 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"`

18
gradlew.bat vendored
View File

@ -1,3 +1,19 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@ -14,7 +30,7 @@ set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@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=
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome

View File

@ -22,32 +22,41 @@
package org.mosad.thecitadelofricks
import org.mosad.thecitadelofricks.CacheController.Companion.courseList
import org.mosad.thecitadelofricks.CacheController.Companion.mensaMenu
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.getTimetable
import org.mosad.thecitadelofricks.controller.CacheController.Companion.mensaMenu
import org.mosad.thecitadelofricks.controller.StatusController.Companion.getStatus
import org.mosad.thecitadelofricks.controller.StatusController.Companion.updateMensaMenuRequests
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
import org.springframework.web.bind.annotation.RequestParam
import org.springframework.web.bind.annotation.RestController
import java.net.HttpURLConnection
import java.net.URL
import java.time.LocalDateTime
import java.util.*
import kotlin.collections.ArrayList
import kotlin.collections.HashSet
@RestController
class APIController {
private val logger: Logger = LoggerFactory.getLogger(APIController::class.java)
private val cache = CacheController()
private val softwareVersion = "1.1.3"
private val apiVersion = "1.1.1"
private val startTime = System.currentTimeMillis() / 1000
companion object {
const val apiVersion = "1.1.4"
const val softwareVersion = "1.1.6"
val startTime = System.currentTimeMillis() / 1000
}
private var requestCount = 0
init {
CacheController() // initialize the CacheController
}
// TODO remove this with API version 1.2.0
@Deprecated("courses is replaced by courseList", replaceWith = ReplaceWith("courseList()"))
@RequestMapping("/courses")
fun courses(): CourseList {
@ -57,15 +66,16 @@ class APIController {
@RequestMapping("/courseList")
fun courseList(): CourseList {
logger.info("courseList request at ${LocalDateTime.now()}!")
requestCount++
return courseList
updateTotalRequests()
return courseList
}
@RequestMapping("/mensamenu")
fun mensamenu(): MensaMenu {
logger.info("mensamenu request at ${LocalDateTime.now()}!")
requestCount++
return mensaMenu
updateTotalRequests()
updateMensaMenuRequests()
return mensaMenu
}
@RequestMapping("/timetable")
@ -74,8 +84,9 @@ class APIController {
@RequestParam(value = "week", defaultValue = "0") week: Int
): TimetableCourseWeek {
logger.info("timetable request at ${LocalDateTime.now()}!")
requestCount++
return cache.getTimetable(courseName, week)
updateTotalRequests()
updateTimetableRequests(courseName)
return getTimetable(courseName, week)
}
@RequestMapping("/lessonSubjectList")
@ -84,95 +95,31 @@ class APIController {
@RequestParam(value = "week", defaultValue = "0") week: Int
): HashSet<String> {
logger.info("lessonSubjectList request at ${LocalDateTime.now()}!")
requestCount++
updateTotalRequests()
return getLessonSubjectList(courseName, week)
}
@RequestMapping("/lesson")
@RequestMapping("/lessons")
fun lesson(
@RequestParam(value = "courseName", defaultValue = "AI4") courseName: String,
@RequestParam(value = "lessonSubject", defaultValue = "Mathematik 4") lessonSubject: String,
@RequestParam(value = "week", defaultValue = "0") week: Int
): ArrayList<Lesson> {
logger.info("lesson request at ${LocalDateTime.now()}!")
requestCount++
updateTotalRequests()
return getLesson(courseName, lessonSubject, week)
}
@RequestMapping("/status")
fun status(): Status {
logger.info("status request at ${LocalDateTime.now()}!")
return getStatus()
}
// non direct api functions
private fun getLessonSubjectList(courseName: String, week: Int): HashSet<String> {
val lessonSubjectList = ArrayList<String>()
// get every lesson subject for the given week
val flatMap = cache.getTimetable(courseName, week).timetable.days.flatMap { it.timeslots.asIterable() }
flatMap.forEach {
it.stream().filter { x -> x.lessonSubject.isNotEmpty() }.findAny().ifPresent { x -> lessonSubjectList.add(x.lessonSubject) }
}
return HashSet(lessonSubjectList)
}
private fun getLesson(courseName: String, lessonSubject: String, week: Int): ArrayList<Lesson> {
val lessonList = ArrayList<Lesson>()
// get all lessons from the weeks timetable
val flatMap = cache.getTimetable(courseName, week).timetable.days.flatMap { it.timeslots.asIterable() }
flatMap.forEach {
it.forEach { lesson ->
if(lesson.lessonSubject.contains(lessonSubject)) {
lessonList.add(lesson)
}
}
//it.stream().filter { x -> x.lessonSubject.contains(lessonSubject) }.findAny().ifPresent { x -> println("${x.lessonSubject}, ${x.lessonTeacher}") }
}
return lessonList
}
private fun getStatus(): Status {
val currentTime = System.currentTimeMillis() / 1000
val minutes = (currentTime - startTime) % 3600 / 60
val hours = (currentTime - startTime) % 86400 / 3600
val days = (currentTime - startTime) / 86400
var hsoCode = 999
var swfrCode = 999
logger.info("status request at ${LocalDateTime.now()}!")
try {
val hsoURL = URL("https://www.hs-offenburg.de/")
val swfrURL = URL("https://www.swfr.de/")
var connection = hsoURL.openConnection() as HttpURLConnection
connection.requestMethod = "HEAD"
connection.connectTimeout = 15000
hsoCode = connection.responseCode
connection = swfrURL.openConnection() as HttpURLConnection
connection.connectTimeout = 15000
swfrCode = connection.responseCode
} catch (e: Exception) {
logger.error("Error while fetching url response codes!", e)
}
return Status(
LocalDateTime.now(),
"$days days, $hours:$minutes",
apiVersion,
softwareVersion,
requestCount,
Date(courseList.meta.updateTime * 1000),
Date(mensaMenu.meta.updateTime * 1000),
hsoCode,
swfrCode
)
@RequestMapping("/health")
fun health(): Int {
logger.info("health request at ${LocalDateTime.now()}!")
return 200
}
}

View File

@ -1,186 +0,0 @@
/**
* TheCitadelofRicks
*
* Copyright 2019 <seil0@mosad.xyz>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301, USA.
*
*/
package org.mosad.thecitadelofricks
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.async
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import org.mosad.thecitadelofricks.hsoparser.CourseListParser
import org.mosad.thecitadelofricks.hsoparser.MensaParser
import org.mosad.thecitadelofricks.hsoparser.TimetableParser
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import java.util.*
import kotlin.collections.ArrayList
import kotlin.concurrent.scheduleAtFixedRate
class CacheController {
private val logger: Logger = LoggerFactory.getLogger(CacheController::class.java)
// hso parser links (hardcoded)
private val courseListURL = "https://www.hs-offenburg.de/studium/vorlesungsplaene/"
private val mensaMenuURL = "https://www.swfr.de/de/essen-trinken/speiseplaene/mensa-offenburg/"
private val mensaName = "Offenburg"
// cache objects
companion object{
lateinit var courseList: CourseList
lateinit var mensaMenu: MensaMenu
var timetableList = ArrayList<TimetableCourseWeek>() // this list contains all timetables
}
init {
initUpdates()
scheduledUpdates()
}
fun getTimetable(courseName: String, week: Int): TimetableCourseWeek = runBlocking {
val currentTime = System.currentTimeMillis() / 1000
var timetable = TimetableWeek()
// check if the timetable already exists and is up to date
when (timetableList.stream().filter { x -> x.meta.courseName == courseName && x.meta.week == week }.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 timetableMeta = TimetableCourseMeta(currentTime, courseName, week, courseLink.replace("week=0","week=$week"))
val jobTimetable = GlobalScope.async {
timetable = TimetableParser().getTimeTable(timetableMeta.link)
}
jobTimetable.await()
timetableList.add(TimetableCourseWeek(timetableMeta, timetable))
logger.info("added new timetable for $courseName, week $week")
}
}
return@runBlocking timetableList.stream().filter { x -> x.meta.courseName == courseName && x.meta.week == week }.findAny().orElse(null)
}
/**
* this function updates the courseList
* during the update process the old data will be returned for a API request
*/
private fun asyncUpdateCourseList() = GlobalScope.launch {
val result = CourseListParser().getCourseLinks(courseListURL)
if (result != null) {
courseList = CourseList(CourseMeta(System.currentTimeMillis() / 1000, result.size), result)
}
logger.info("updated courses successful at ${Date(courseList.meta.updateTime * 1000)}")
}
/**
* this function updates the mensa menu list
* during the update process the old data will be returned for a API request
*/
private fun asyncUpdateMensa() = GlobalScope.launch {
val mensaCurrentWeek = MensaParser().getMensaMenu(mensaMenuURL)
val mensaNextWeek = MensaParser().getMensaMenu(MensaParser().getMenuLinkNextWeek(mensaMenuURL))
// only update if we get valid data
if (mensaCurrentWeek != null && mensaNextWeek != null) {
mensaMenu = MensaMenu(MensaMeta(System.currentTimeMillis() / 1000, mensaName), mensaCurrentWeek, mensaNextWeek)
}
logger.info("updated mensamenu successful at ${Date(mensaMenu.meta.updateTime * 1000)}")
}
/**
* this function updates all existing timetables
* during the update process the old data will be returned for a API request
*/
private fun asyncUpdateTimetables() = GlobalScope.launch {
timetableList.forEach { timetableCourse ->
val updateURL = timetableCourse.meta.link
timetableCourse.timetable = TimetableParser().getTimeTable(updateURL)
timetableCourse.meta.updateTime = System.currentTimeMillis() / 1000
}
logger.info("updated ${timetableList.size} timetables successful!")
}
/**
* before the APIController is up, get the data fist
* runBlocking: otherwise the api would return no data to requests for a few seconds after startup
*/
private fun initUpdates() = runBlocking {
// get all courses on startup
val jobCourseUpdate = GlobalScope.async {
val result = CourseListParser().getCourseLinks(courseListURL)
if (result != null) {
courseList = CourseList(CourseMeta(System.currentTimeMillis() / 1000, result.size), result)
}
}
// get the current and next weeks mensa menus
val jobMensa = GlobalScope.async{
val mensaCurrentWeek = MensaParser().getMensaMenu(mensaMenuURL)
val mensaNextWeek = MensaParser().getMensaMenu(MensaParser().getMenuLinkNextWeek(mensaMenuURL))
// only update if we get valid data
if (mensaCurrentWeek != null && mensaNextWeek != null) {
mensaMenu = MensaMenu(MensaMeta(System.currentTimeMillis() / 1000, mensaName), mensaCurrentWeek, mensaNextWeek)
}
}
jobCourseUpdate.await()
jobMensa.await()
logger.info("init updates successful")
}
/**
* doesn't account the change between winter and summer time!
*
*/
private fun scheduledUpdates() {
val currentTime = System.currentTimeMillis()
val initDelay24h = (86400000 - ((currentTime + 3600000) % 86400000)) + 60000
val initDelay3h = (10800000 - ((currentTime + 3600000) % 10800000)) + 60000
val initDelay1h = (3600000 - ((currentTime + 3600000) % 10800000)) + 60000
println(initDelay1h / 1000)
// 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()
}
}
}

View File

@ -41,8 +41,7 @@ data class MensaWeek(val days: Array<Meals> = Array(7) { Meals(ArrayList()) })
data class MensaMeta(val updateTime: Long, val mensaName: String)
data class MensaMenu(val meta: MensaMeta, val currentWeek: MensaWeek, val nextWeek: MensaWeek) {
}
data class MensaMenu(val meta: MensaMeta, val currentWeek: MensaWeek, val nextWeek: MensaWeek)
// data classes for the timetable part
data class Lesson(
@ -57,18 +56,23 @@ data class TimetableDay(val timeslots: Array<ArrayList<Lesson>> = Array(6) { Arr
data class TimetableWeek(val days: Array<TimetableDay> = Array(6) { TimetableDay() })
data class TimetableCourseMeta(var updateTime: Long, val courseName: String, val week: Int, val link: String)
data class TimetableCourseMeta(var updateTime: Long, val courseName: String, val weekIndex: Int, val weekNumberYear: Int, val link: String)
data class TimetableCourseWeek(val meta: TimetableCourseMeta, var timetable: 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 mensaMenuRequests: Int,
val timetableRequests: ArrayList<TimetableCounter>,
val timetableListSize: Int,
val coursesLastUpdate: Date,
val mensaLastUpdate: Date,
val hsoResponseCode: Int,

View File

@ -0,0 +1,275 @@
/**
* 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.controller
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.async
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import org.mosad.thecitadelofricks.*
import org.mosad.thecitadelofricks.hsoparser.CourseListParser
import org.mosad.thecitadelofricks.hsoparser.MensaParser
import org.mosad.thecitadelofricks.hsoparser.TimetableParser
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import java.util.*
import kotlin.collections.ArrayList
import kotlin.concurrent.scheduleAtFixedRate
class CacheController {
private val logger: Logger = LoggerFactory.getLogger(CacheController::class.java)
// hso parser links (hardcoded)
private val courseListURL = "https://www.hs-offenburg.de/studium/vorlesungsplaene/"
private val mensaMenuURL = "https://www.swfr.de/de/essen-trinken/speiseplaene/mensa-offenburg/"
private val mensaName = "Offenburg"
init {
initUpdates()
scheduledUpdates()
}
// cache objects
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
/**
* get a timetable, since they may not cached, we need to make sure it's cached, otherwise download
* @param courseName the name of the course to be requested
* @param weekIndex request week number (current week = 0)
* @return the timetable of a course (courseName)
*/
fun getTimetable(courseName: String, weekIndex: Int): TimetableCourseWeek = runBlocking {
val currentTime = System.currentTimeMillis() / 1000
var timetable = TimetableWeek()
var weekNumberYear = 0
// check if the timetable already exists and is up to date
when (timetableList.stream().filter { x -> x.meta.courseName == courseName && x.meta.weekIndex == weekIndex }.findAny().orElse(null)) {
// there is no such course yet, create one
null -> {
val courseLink = courseList.courses.stream().filter { x -> x.courseName == courseName }.findFirst().orElse(null).courseLink
val timetableLink = courseLink.replace("week=0","week=$weekIndex")
val jobTimetable = GlobalScope.async {
timetable = TimetableParser().getTimeTable(timetableLink)
weekNumberYear = TimetableParser().getWeekNumberYear(timetableLink)
}
jobTimetable.await()
timetableList.add(
TimetableCourseWeek(
TimetableCourseMeta(
currentTime,
courseName,
weekIndex,
weekNumberYear,
timetableLink
),
timetable
)
)
logger.info("added new timetable for $courseName, week $weekIndex")
}
}
return@runBlocking timetableList.stream().filter { x -> x.meta.courseName == courseName && x.meta.weekIndex == weekIndex }.findAny().orElse(null)
}
/**
* get every explicit lesson in a week
* @param courseName the name of the course to be requested
* @param weekIndex request week number (current week = 0)
* @return a HashSet of explicit lessons for one week
*/
fun getLessonSubjectList(courseName: String, weekIndex: Int): HashSet<String> = runBlocking {
val lessonSubjectList = ArrayList<String>()
// get every lesson subject for the given week
val flatMap = getTimetable(courseName, weekIndex).timetable.days.flatMap { it.timeslots.asIterable() }
flatMap.forEach {
it.stream().filter { x -> x.lessonSubject.isNotEmpty() }.findAny().ifPresent { x -> lessonSubjectList.add(x.lessonSubject) }
}
return@runBlocking HashSet(lessonSubjectList)
}
/**
* get every explicit lesson 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 of every lesson with lessonSubject for one week
*/
fun getLesson(courseName: String, lessonSubject: String, weekIndex: Int): ArrayList<Lesson> {
val lessonList = ArrayList<Lesson>()
// get all lessons from the weeks timetable
val flatMap = getTimetable(courseName, weekIndex).timetable.days.flatMap { it.timeslots.asIterable() }
flatMap.forEach {
it.forEach { lesson ->
if(lesson.lessonSubject.contains(lessonSubject)) {
lessonList.add(lesson)
}
}
// TODO Java 11
//it.stream().filter { x -> x.lessonSubject.contains(lessonSubject) }.findAny().ifPresent { x -> println("${x.lessonSubject}, ${x.lessonTeacher}") }
}
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(courseListURL)?.let {
courseList =
CourseList(
CourseMeta(
System.currentTimeMillis() / 1000,
it.size
), it
)
}
logger.info("updated courses successful at ${Date(courseList.meta.updateTime * 1000)}")
}
/**
* this function updates the mensa menu list
* during the update process the old data will be returned for a API request
*/
private fun asyncUpdateMensa() = GlobalScope.launch {
val mensaCurrentWeek = MensaParser().getMensaMenu(mensaMenuURL)
val mensaNextWeek = MensaParser().getMensaMenu(MensaParser().getMenuLinkNextWeek(mensaMenuURL))
// only update if we get valid data
if (mensaCurrentWeek != null && mensaNextWeek != null) {
mensaMenu =
MensaMenu(
MensaMeta(
System.currentTimeMillis() / 1000,
mensaName
), mensaCurrentWeek, mensaNextWeek
)
}
logger.info("updated mensamenu successful at ${Date(mensaMenu.meta.updateTime * 1000)}")
}
/**
* this function updates all existing timetables
* during the update process the old data will be returned for a API request
*/
private fun asyncUpdateTimetables() = GlobalScope.launch {
timetableList.forEach { timetableCourse ->
val updateURL = timetableCourse.meta.link
timetableCourse.timetable = TimetableParser().getTimeTable(updateURL)
timetableCourse.meta.updateTime = System.currentTimeMillis() / 1000
}
logger.info("updated ${timetableList.size} timetables successful!")
}
/**
* before the APIController is up, get the data fist
* runBlocking: otherwise the api would return no data to requests for a few seconds after startup
*/
private fun initUpdates() = runBlocking {
// get all courses on startup
val jobCourseUpdate = GlobalScope.async {
CourseListParser().getCourseLinks(courseListURL)?.let {
courseList =
CourseList(
CourseMeta(
System.currentTimeMillis() / 1000,
it.size
), it
)
}
}
// get the current and next weeks mensa menus
val jobMensa = GlobalScope.async{
val mensaCurrentWeek = MensaParser().getMensaMenu(mensaMenuURL)
val mensaNextWeek = MensaParser().getMensaMenu(MensaParser().getMenuLinkNextWeek(mensaMenuURL))
// only update if we get valid data
if (mensaCurrentWeek != null && mensaNextWeek != null) {
mensaMenu =
MensaMenu(
MensaMeta(
System.currentTimeMillis() / 1000,
mensaName
), mensaCurrentWeek, mensaNextWeek
)
}
}
jobCourseUpdate.await()
jobMensa.await()
logger.info("init updates successful")
}
/**
* doesn't account the change between winter and summer time!
*
*/
private fun scheduledUpdates() {
val currentTime = System.currentTimeMillis()
val initDelay24h = (86400000 - ((currentTime + 3600000) % 86400000)) + 60000
val initDelay3h = (10800000 - ((currentTime + 3600000) % 10800000)) + 60000
val initDelay1h = (3600000 - ((currentTime + 3600000) % 3600000)) + 60000
// update courseList every 24 hours (time in ms)
Timer().scheduleAtFixedRate(initDelay24h, 86400000) {
asyncUpdateCourseList()
}
// update all already existing timetables every 3 hours (time in ms)
Timer().scheduleAtFixedRate(initDelay3h, 10800000) {
asyncUpdateTimetables()
}
// update courses every hour (time in ms)
Timer().scheduleAtFixedRate(initDelay1h, 3600000) {
asyncUpdateMensa()
}
// post to status.mosad.xyz every hour
Timer().scheduleAtFixedRate(initDelay1h, 3600000) {
CachetAPIController.postTotalRequests()
}
}
}

View File

@ -0,0 +1,77 @@
/**
* 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.controller
import org.mosad.thecitadelofricks.controller.StatusController.Companion.getTotalRequests
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
class CachetAPIController {
companion object {
private val logger: Logger = LoggerFactory.getLogger(CachetAPIController::class.java)
private const val baseURL = "https://status.mosad.xyz"
private const val apiKey = "" //TODO
private var oldTotalRequests = 0
fun postTotalRequests() {
try {
val url = URL("$baseURL/api/v1/metrics/1/points")
val jsonInputString = "{\"value\": ${getTotalRequests() -oldTotalRequests}, \"timestamp\": \"${(System.currentTimeMillis() / 1000)}\"}"
oldTotalRequests = getTotalRequests()
val con = url.openConnection() as HttpURLConnection
con.requestMethod = "POST"
con.setRequestProperty("Content-Type", "application/json; utf-8")
con.setRequestProperty("Accept", "application/json")
con.setRequestProperty("X-Cachet-Token", apiKey)
con.doOutput = true
val os = con.outputStream
val input = jsonInputString.toByteArray(charset("utf-8"))
os.write(input, 0, input.size)
val br = BufferedReader(InputStreamReader(con.inputStream, "utf-8"))
val response = StringBuilder()
var responseLine: String?
while (br.readLine().also { responseLine = it } != null) {
response.append(responseLine!!.trim { it <= ' ' })
}
logger.info(response.toString())
} catch (e1: IOException) {
e1.printStackTrace()
}
}
}
}

View File

@ -0,0 +1,127 @@
/**
* 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.controller
import org.mosad.thecitadelofricks.APIController.Companion.apiVersion
import org.mosad.thecitadelofricks.APIController.Companion.softwareVersion
import org.mosad.thecitadelofricks.APIController.Companion.startTime
import org.mosad.thecitadelofricks.Status
import org.mosad.thecitadelofricks.TimetableCounter
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import java.net.HttpURLConnection
import java.net.URL
import java.time.LocalDateTime
import java.util.*
import kotlin.collections.ArrayList
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>()
fun updateTotalRequests() {
totalRequests++
}
fun updateMensaMenuRequests() {
mensaMenuRequests++
}
/**
* 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++
// }, {
//
// })
}
fun getTotalRequests(): Int {
return totalRequests
}
fun getMensaMenuRequests(): Int {
return mensaMenuRequests
}
fun getTimetableRequests(): ArrayList<TimetableCounter> {
return timetableRequests
}
fun getStatus(): Status {
val currentTime = System.currentTimeMillis() / 1000
val minutes = (currentTime - startTime) % 3600 / 60
val hours = (currentTime - startTime) % 86400 / 3600
val days = (currentTime - startTime) / 86400
var hsoCode = 999
var swfrCode = 999
try {
val hsoURL = URL("https://www.hs-offenburg.de/")
val swfrURL = URL("https://www.swfr.de/")
var connection = hsoURL.openConnection() as HttpURLConnection
connection.requestMethod = "HEAD"
connection.connectTimeout = 15000
hsoCode = connection.responseCode
connection = swfrURL.openConnection() as HttpURLConnection
connection.connectTimeout = 15000
swfrCode = connection.responseCode
} catch (e: Exception) {
logger.error("Error while fetching url response codes!", e)
}
return Status(
LocalDateTime.now(),
"$days days, $hours:$minutes",
apiVersion,
softwareVersion,
getTotalRequests(),
getMensaMenuRequests(),
getTimetableRequests(),
CacheController.timetableList.size,
Date(CacheController.courseList.meta.updateTime * 1000),
Date(CacheController.mensaMenu.meta.updateTime * 1000),
hsoCode,
swfrCode
)
}
}
}

View File

@ -23,26 +23,43 @@
package org.mosad.thecitadelofricks.hsoparser
import org.jsoup.Jsoup
import org.jsoup.nodes.Document
import org.mosad.thecitadelofricks.Meal
import org.mosad.thecitadelofricks.MensaWeek
import org.slf4j.LoggerFactory
import java.net.SocketTimeoutException
class MensaParser {
var logger: org.slf4j.Logger = LoggerFactory.getLogger(MensaParser::class.java)
private var logger: org.slf4j.Logger = LoggerFactory.getLogger(MensaParser::class.java)
/**
* returns the mensa menu for a week
* returns the mensa's menu for a week
* @param mensaMenuURL the url to a mensa menu (swfr)
* @return the menu plan found at menuURL or null if the request was not successful
* @return the mensa's menu as MensaWeek found at menuURL or null if the request was not successful
*/
fun getMensaMenu(mensaMenuURL: String): MensaWeek? {
val mealWeekList = MensaWeek()
try {
return try {
val menuHTML = Jsoup.connect(mensaMenuURL).timeout(15000).get()
parseMensaMenu(menuHTML)
} catch (ex: SocketTimeoutException) {
logger.warn("timeout from $mensaMenuURL, updating on next attempt!")
null
} catch (gex: Exception) {
logger.error("general MensaParser error", gex)
null
}
}
menuHTML.select("#speiseplan-tabs").select("div.tab-content").select("div.menu-tagesplan")
/**
* parse the mensa's menu from the html document
* @param htmlDoc the html document containing the menu
* @return the mensa's menu as MensaWeek
*/
fun parseMensaMenu(htmlDoc: Document): MensaWeek? {
val mealWeekList = MensaWeek()
try {
htmlDoc.select("#speiseplan-tabs").select("div.tab-content").select("div.menu-tagesplan")
.forEachIndexed { dayIndex, day ->
val strDay = day.select("h3").text()
@ -56,11 +73,8 @@ class MensaParser {
}
}
} catch (ex: SocketTimeoutException) {
logger.warn("timeout from $mensaMenuURL, updating on next attempt!")
return null
} catch (gex: Exception) {
logger.error("general MensaParser error", gex)
} catch (pex: Exception) {
logger.error("error while parsing the html file", pex)
return null
}

View File

@ -23,10 +23,13 @@
package org.mosad.thecitadelofricks.hsoparser
import org.jsoup.Jsoup
import org.jsoup.nodes.Document
import org.mosad.thecitadelofricks.Lesson
import org.mosad.thecitadelofricks.TimetableWeek
import org.slf4j.LoggerFactory
class TimetableParser {
private var logger: org.slf4j.Logger = LoggerFactory.getLogger(TimetableParser::class.java)
private val days = arrayOf("Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday")
/**
@ -36,13 +39,18 @@ class TimetableParser {
* @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()
}
}
fun parseTimeTable(htmlDoc: Document): TimetableWeek {
val timetableWeek = TimetableWeek()
val scheduleHTML = Jsoup.connect(timetableURL).get() // TODO add a try catch block to cover timeouts
val rows = htmlDoc.select("table.timetable").select("tr[scope=\"row\"]")
//val week = scheduleHTML.select("h1.timetable-caption").text()
//println("$week successful!\n")
val rows = scheduleHTML.select("table.timetable").select("tr[scope=\"row\"]")
var sDay = -1
var sRow = -1
var sLesson = Lesson("", "", "", "", "")
@ -105,11 +113,27 @@ class TimetableParser {
}
//printTimetableWeek(timetableWeek)
return timetableWeek
}
/**
* get the week number of the year for the timetable
* @param timetableURL the URL of the timetable you want to get
*/
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()
}
@Suppress("unused")
/**
* print a timetable

View File

@ -0,0 +1,66 @@
/**
* 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.junit.jupiter.api.Assertions
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/"
@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)
// 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()
)
}
@Test
fun parseMensaMenuEmptyWeek() {
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)
Assertions.assertEquals(
"MensaWeek(days=[Meals(meals=[]), Meals(meals=[]), Meals(meals=[]), Meals(meals=[]), Meals(meals=[]), Meals(meals=[]), Meals(meals=[])])",
mensaWeek.toString()
)
}
// TODO add test for special days ie. public holiday
@Test
fun getMenuLinkNextWeek() {
val urlNextWeek = MensaParser().getMenuLinkNextWeek(mensaMenuURL) // this need a connection to the swfr server
Assertions.assertNotNull(urlNextWeek)
}
}

View File

@ -0,0 +1,60 @@
/**
* 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.junit.jupiter.api.Assertions
import org.junit.jupiter.api.Test
import java.io.File
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()
Assertions.assertEquals(expectedTimetable, actualTimetable)
}
@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()
Assertions.assertEquals(expectedTimetable, actualTimetable)
}
@Test
fun parseWeekNumberYear() {
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)
Assertions.assertEquals(42, actualWeekNumberYear)
}
}

View File

@ -0,0 +1 @@
TimetableWeek(days=[TimetableDay(timeslots=[[], [], [], [], [], []]), TimetableDay(timeslots=[[], [], [], [], [], []]), TimetableDay(timeslots=[[], [], [], [], [], []]), TimetableDay(timeslots=[[], [], [], [], [], []]), TimetableDay(timeslots=[[], [], [], [], [], []]), TimetableDay(timeslots=[[], [], [], [], [], []])])

View File

@ -0,0 +1 @@
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=)]])])

View File

@ -0,0 +1,134 @@
<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&amp;tx_swfrspeiseplan_pi1%5Baction%5D=show&amp;tx_swfrspeiseplan_pi1%5Bcontroller%5D=Speiseplan&amp;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&amp;tx_swfrspeiseplan_pi1%5BweekToShow%5D=0&amp;tx_swfrspeiseplan_pi1%5Bcolored%5D=1&amp;tx_swfrspeiseplan_pi1%5Bort%5D=651&amp;tx_swfrspeiseplan_pi1%5Baction%5D=buildPdf&amp;tx_swfrspeiseplan_pi1%5Bcontroller%5D=Speiseplan&amp;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&amp;tx_swfrspeiseplan_pi1%5BweekToShow%5D=0&amp;tx_swfrspeiseplan_pi1%5Bcolored%5D=0&amp;tx_swfrspeiseplan_pi1%5Bort%5D=651&amp;tx_swfrspeiseplan_pi1%5Baction%5D=buildPdf&amp;tx_swfrspeiseplan_pi1%5Bcontroller%5D=Speiseplan&amp;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&amp;tx_swfrspeiseplan_pi1%5BweekToShow%5D=0&amp;tx_swfrspeiseplan_pi1%5Bcolored%5D=1&amp;tx_swfrspeiseplan_pi1%5Bort%5D=651&amp;tx_swfrspeiseplan_pi1%5Baction%5D=buildPdfAllergenes&amp;tx_swfrspeiseplan_pi1%5Bcontroller%5D=Speiseplan&amp;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&amp;tx_swfrspeiseplan_pi1%5BweekToShow%5D=0&amp;tx_swfrspeiseplan_pi1%5Bcolored%5D=0&amp;tx_swfrspeiseplan_pi1%5Bort%5D=651&amp;tx_swfrspeiseplan_pi1%5Baction%5D=buildPdfAllergenes&amp;tx_swfrspeiseplan_pi1%5Bcontroller%5D=Speiseplan&amp;cHash=b0d4e30323a2010310bbb6cad8b55fe9"><i
class="fas fa-file-download" aria-hidden="true"></i> Wochenplan s/w</a></p>
</div>
</div>
<div class="col-sm-4 pt-3">
<p class="hide-with-allergenes"><a href="#" data-toggle="modal" data-target="#allergenesModal"><i
class="fas fa-redo-alt"></i> Allergiehinweise anzeigen</a></p>
<p class="show-with-allergenes"><a href="#" class="btn-hide-allergenes"><i class="fas fa-undo-alt"></i>
Allergiehinweise verbergen</a></p>
</div>
</div>
<div class="tab-content">
<div id="tab-mon" class="menu-tagesplan tab-pane fade">
<h3>Montag 12.08.</h3>
<div class="row row-narrow row-buffer row-table">
<div class="col-md-1 bg-beige-hell zusatzangaben">
<i class="glyphicons glyphicons-circle-info"></i>
</div>
<div class="col-md-11 border-beige-hell">
<h4>heute keine Essensausgabe</h4>
</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>
<div class="col-md-11 border-beige-hell">
<h4>heute keine Essensausgabe</h4>
</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>
<div class="col-md-11 border-beige-hell">
<h4>heute keine Essensausgabe</h4>
</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>

View File

@ -0,0 +1,518 @@
<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&amp;tx_swfrspeiseplan_pi1%5Baction%5D=show&amp;tx_swfrspeiseplan_pi1%5Bcontroller%5D=Speiseplan&amp;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&amp;tx_swfrspeiseplan_pi1%5BweekToShow%5D=0&amp;tx_swfrspeiseplan_pi1%5Bcolored%5D=1&amp;tx_swfrspeiseplan_pi1%5Bort%5D=651&amp;tx_swfrspeiseplan_pi1%5Baction%5D=buildPdf&amp;tx_swfrspeiseplan_pi1%5Bcontroller%5D=Speiseplan&amp;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&amp;tx_swfrspeiseplan_pi1%5BweekToShow%5D=0&amp;tx_swfrspeiseplan_pi1%5Bcolored%5D=0&amp;tx_swfrspeiseplan_pi1%5Bort%5D=651&amp;tx_swfrspeiseplan_pi1%5Baction%5D=buildPdf&amp;tx_swfrspeiseplan_pi1%5Bcontroller%5D=Speiseplan&amp;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&amp;tx_swfrspeiseplan_pi1%5BweekToShow%5D=0&amp;tx_swfrspeiseplan_pi1%5Bcolored%5D=1&amp;tx_swfrspeiseplan_pi1%5Bort%5D=651&amp;tx_swfrspeiseplan_pi1%5Baction%5D=buildPdfAllergenes&amp;tx_swfrspeiseplan_pi1%5Bcontroller%5D=Speiseplan&amp;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&amp;tx_swfrspeiseplan_pi1%5BweekToShow%5D=0&amp;tx_swfrspeiseplan_pi1%5Bcolored%5D=0&amp;tx_swfrspeiseplan_pi1%5Bort%5D=651&amp;tx_swfrspeiseplan_pi1%5Baction%5D=buildPdfAllergenes&amp;tx_swfrspeiseplan_pi1%5Bcontroller%5D=Speiseplan&amp;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>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long