Merge branch 'refs/heads/development' into 49-add-htwkarte-linkout-at-ics-event-info

# Conflicts:
#	services/ical/main.go
#	services/ical/service/ical/ical.go
#	services/ical/service/routes.go
This commit is contained in:
Elmar Kresse
2024-10-17 10:54:29 +02:00
26 changed files with 883 additions and 40 deletions

View File

@ -33,6 +33,8 @@ services:
- DATA_MANAGER_URL=htwkalender-data-manager
networks:
- "net"
depends_on:
- htwkalender-data-manager
htwkalender-frontend:
image: DOCKER_REGISTRY_REPO-frontend # DOCKER_REGISTRY_REPO will be replaced by CI

View File

@ -33,6 +33,8 @@ services:
- DATA_MANAGER_URL=htwkalender-data-manager
networks:
- "net"
depends_on:
- htwkalender-data-manager
htwkalender-frontend:
image: DOCKER_REGISTRY_REPO-frontend # DOCKER_REGISTRY_REPO will be replaced by CI

View File

@ -35,6 +35,8 @@ services:
target: dev # prod
environment:
- DATA_MANAGER_URL=htwkalender-data-manager
depends_on:
- htwkalender-data-manager
htwkalender-frontend:
build:

View File

@ -27,8 +27,10 @@ http {
include mime.types;
default_type application/octet-stream;
access_log /opt/bitnami/nginx/logs/proxy_access.log;
error_log /opt/bitnami/nginx/logs/proxy_error.log;
log_format anonymized '[$time_local] "$request" $status $body_bytes_sent "$http_referer"';
access_log /opt/bitnami/nginx/logs/proxy_access.log anonymized;
error_log /opt/bitnami/nginx/logs/proxy_error.log error;
sendfile on;
keepalive_timeout 180s;

View File

@ -142,7 +142,10 @@
"copyToastErrorDetail": "Link konnte nicht in Zwischenablage kopiert werden",
"copyToClipboard": "Link kopieren",
"toGoogleCalendar": "Google Kalender",
"toMicrosoftCalendar": "Microsoft Kalender"
"toMicrosoftCalendar": "Microsoft Kalender",
"copyTokenToastSummary": "Information",
"copyTokenToastNotification": "Token in die Zwischenablage kopiert, um den Kalender in der HTWK App zu abonnieren",
"toHTWKApp": "HTWK App"
},
"calendarPreview": {
"preview": "Vorschau",
@ -229,6 +232,14 @@
"six": "Sonstige Einstellungen nach Belieben vornehmen.",
"seven": "Auf “Subscribe to calendar” klicken.",
"eight": "Das Windows-Phone-Gerät muss mit dem gleichen Outlook.com-Benutzerkonto angemeldet sein. Fortan sollte die Synchronisierung des Kalenders automatisch erfolgen."
},
"htwk_app": {
"description": "Die HTWK-App bietet die Möglichkeit, den Stundenplan direkt in der App zu abonnieren. Dazu musst du nur den Token in der App einfügen und schon hast du deinen Stundenplan immer dabei. ",
"one": "Erstelle deinen Kalender und kopiere den Token.",
"two": "Öffne die HTWK-App und gehe in den Kalender.",
"three": "Dort fügst du den Token ein und schon hast du deinen Stundenplan immer dabei.",
"four": "Falls du bereits einen Kalender in der App abonniert hast, kannst du über den Button oben rechts den Kalender wechseln.",
"five": "Der Kalender in der HTWK App wird über einen weiteren Service der App stündlich aktualisiert. Wenn du den Kalender bearbeitest werden die Änderungen erst nach einer Stunde in der App sichtbar."
}
},
"fourthQuestion": "Kalender abonnieren? Ich will den downloaden!",

View File

@ -142,7 +142,10 @@
"copyToastErrorDetail": "could not copy link to clipboard",
"copyToClipboard": "copy to clipboard",
"toGoogleCalendar": "to Google Calendar",
"toMicrosoftCalendar": "to Microsoft Calendar"
"toMicrosoftCalendar": "to Microsoft Calendar",
"copyTokenToastSummary": "information",
"copyTokenToastNotification": "token copied to clipboard, go to the HTWK app to insert it",
"toHTWKApp": "HTWK App"
},
"calendarPreview": {
"preview": "preview",
@ -229,6 +232,14 @@
"six": "Make other settings as desired.",
"seven": "Click on 'Subscribe to calendar.'",
"eight": "The Windows Phone device must be logged in with the same Outlook.com user account. From now on, calendar synchronization should occur automatically."
},
"htwk_app": {
"description": "The HTWK app offers the possibility to subscribe to the timetable directly in the app. All you have to do is insert the token in the app and you'll always have your timetable with you. ",
"one": "Create your calendar and copy the token.",
"two": "Open the HTWK app and go to the calendar.",
"three": "Paste the token there and you'll always have your timetable with you.",
"four": "If you have already subscribed to a calendar in the app, you can change the calendar using the button at the top right.",
"five": "The calendar in the HTWK app is updated hourly via another service of the app. If you edit the calendar, the changes will only be visible in the app after one hour."
}
},
"fourthQuestion": "Subscribe to the calendar? I want to download it!",

View File

@ -78,6 +78,17 @@ const forwardToMicrosoft = () => {
);
};
const copyTokenToClipboard = () => {
navigator.clipboard.writeText(tokenStore().token).then(() => {
toast.add({
severity: "info",
summary: t("calendarLink.copyTokenToastSummary"),
detail: t("calendarLink.copyTokenToastNotification"),
life: 3000,
});
});
};
const actions = computed(() => [
{
label: t("calendarLink.copyToClipboard"),
@ -94,6 +105,11 @@ const actions = computed(() => [
icon: "pi pi-microsoft",
command: forwardToMicrosoft,
},
{
label: t("calendarLink.toHTWKApp"),
icon: "pi pi-mobile",
command: copyTokenToClipboard,
},
]);
</script>

View File

@ -16,7 +16,8 @@ You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<script lang="ts" setup></script>
<script lang="ts" setup>
</script>
<template>
<div class="flex align-items-center justify-content-center flex-column">
@ -58,6 +59,16 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
</li>
</ol>
</AccordionTab>
<AccordionTab header="HTWK App">
<p>{{ $t("faqView.thirdAnswer.htwk_app.description") }}</p>
<ol>
<li>{{ $t("faqView.thirdAnswer.htwk_app.one") }}</li>
<li>{{ $t("faqView.thirdAnswer.htwk_app.two") }}</li>
<li>{{ $t("faqView.thirdAnswer.htwk_app.three") }}</li>
<li>{{ $t("faqView.thirdAnswer.htwk_app.four") }}</li>
<li>{{ $t("faqView.thirdAnswer.htwk_app.five") }}</li>
</ol>
</AccordionTab>
<AccordionTab
:header="$t('faqView.thirdAnswer.microsoft_outlook.title')"
>

View File

@ -59,8 +59,10 @@ http {
real_ip_header CF-Connecting-IP;
access_log /opt/bitnami/nginx/logs/proxy_access.log;
error_log /opt/bitnami/nginx/logs/proxy_error.log;
log_format anonymized '[$time_local] "$request" $status $body_bytes_sent "$http_referer"';
access_log /opt/bitnami/nginx/logs/proxy_access.log anonymized;
error_log /opt/bitnami/nginx/logs/proxy_error.log error;
sendfile on;
keepalive_timeout 180s;

View File

@ -60,8 +60,10 @@ http {
real_ip_header CF-Connecting-IP;
access_log /opt/bitnami/nginx/logs/proxy_access.log;
error_log /opt/bitnami/nginx/logs/proxy_error.log;
log_format anonymized '[$time_local] "$request" $status $body_bytes_sent "$http_referer"';
access_log /opt/bitnami/nginx/logs/proxy_access.log anonymized;
error_log /opt/bitnami/nginx/logs/proxy_error.log error;
sendfile on;
keepalive_timeout 180s;

View File

@ -10,6 +10,12 @@ events {
http {
include mime.types;
default_type application/octet-stream;
gzip on;
log_format anonymized '[$time_local] "$request" $status $body_bytes_sent "$http_referer"';
access_log /opt/bitnami/nginx/logs/proxy_access.log anonymized;
error_log /opt/bitnami/nginx/logs/proxy_error.log error;
map $request_method $ratelimit_key {
POST $binary_remote_addr;

View File

@ -78,6 +78,42 @@ func AddRoutes(services serviceModel.Service) {
return nil
})
services.App.OnBeforeServe().Add(func(e *core.ServeEvent) error {
_, err := e.Router.AddRoute(echo.Route{
Method: http.MethodGet,
Path: "/api/fetch/group",
Handler: func(c echo.Context) error {
seminarGroupString := c.QueryParam("seminarGroup")
if seminarGroupString == "" {
return c.JSON(http.StatusBadRequest, "Seminar group could not be empty")
} else {
//find seminar group by name
seminarGroup, err := services.CourseService.FindCourseByCourseName(seminarGroupString)
if err != nil {
return c.JSON(http.StatusBadRequest, "Failed to find seminar group")
}
events, err := services.EventService.UpdateModulesForCourse(seminarGroup)
if err != nil {
return c.JSON(http.StatusBadRequest, "Failed to fetch seminar group")
}
return c.JSON(http.StatusOK, events)
}
},
Middlewares: []echo.MiddlewareFunc{
apis.ActivityLogger(services.App),
apis.RequireAdminAuth(),
},
})
if err != nil {
return err
}
return nil
})
services.App.OnBeforeServe().Add(func(e *core.ServeEvent) error {
_, err := e.Router.AddRoute(echo.Route{
Method: http.MethodGet,

View File

@ -146,3 +146,17 @@ func GetAllCoursesForSemesterWithEvents(app *pocketbase.PocketBase, semester str
return courseArray, nil
}
func FindCourseByCourseName(app *pocketbase.PocketBase, courseName string) (model.SeminarGroup, error) {
var course SeminarGroup
// get the course by its name
err := app.Dao().DB().Select("*").From("groups").Where(dbx.NewExp("course = {:course}", dbx.Params{"course": courseName})).One(&course)
if err != nil {
slog.Error("Error while getting group from database: ", "error", err)
return model.SeminarGroup{}, err
}
return course.toSeminarGroupModel(), nil
}

View File

@ -28,6 +28,7 @@ type CourseService interface {
GetAllCourses() []string
GetAllCoursesForSemester(semester string) []model.SeminarGroup
GetAllCoursesForSemesterWithEvents(semester string) ([]string, error)
FindCourseByCourseName(courseName string) (model.SeminarGroup, error)
}
// PocketBaseCourseService is a struct that implements the CourseService interface
@ -72,3 +73,8 @@ func removeEmptyCourses(courses []string) []string {
}
return filteredCourses
}
// FindCourseByCourseName returns a course by its name
func (s *PocketBaseCourseService) FindCourseByCourseName(courseName string) (model.SeminarGroup, error) {
return db.FindCourseByCourseName(s.app, courseName)
}

View File

@ -10,6 +10,11 @@ type MockCourseService struct {
mock.Mock
}
func (m *MockCourseService) FindCourseByCourseName(courseName string) (model.SeminarGroup, error) {
args := m.Called(courseName)
return args.Get(0).(model.SeminarGroup), args.Error(1)
}
func (m *MockCourseService) GetAllCourses() []string {
args := m.Called()
return args.Get(0).([]string)

View File

@ -98,8 +98,13 @@ func parseSeminarGroup(result string) model.SeminarGroup {
}
table := findFirstTable(doc)
eventTables := getEventTables(doc)
allDayLabels := getAllDayLabels(doc)
eventTables := getEventTables(doc, allDayLabels)
if table == nil {
slog.Error("Failed to find first table")
return model.SeminarGroup{}
}
course := findFirstSpanWithClass(table, "header-2-0-1").FirstChild.Data
semesterString := findFirstSpanWithClass(table, "header-0-2-0").FirstChild.Data
@ -113,7 +118,7 @@ func parseSeminarGroup(result string) model.SeminarGroup {
}
}
eventsWithCombinedWeeks := toEvents(eventTables, allDayLabels, course)
eventsWithCombinedWeeks := toEvents(eventTables, course)
splitEventsByWeekVal := splitEventsByWeek(eventsWithCombinedWeeks)
events := splitEventsBySingleWeek(splitEventsByWeekVal)
events = convertWeeksToDates(events, semester, year)
@ -210,18 +215,18 @@ func extractSemesterAndYear(semesterString string) (string, string) {
return semesterShortcut, year
}
func toEvents(tables [][]*html.Node, days []string, course string) []model.Event {
func toEvents(tables map[string][]*html.Node, course string) []model.Event {
var events []model.Event
for table := range tables {
for row := range tables[table] {
for day := range tables {
for row := range tables[day] {
tableData := findTableData(tables[table][row])
tableData := findTableData(tables[day][row])
if len(tableData) > 0 {
start, _ := types.ParseDateTime(createTimeFromHourAndMinuteString(getTextContent(tableData[1])))
end, _ := types.ParseDateTime(createTimeFromHourAndMinuteString(getTextContent(tableData[2])))
events = append(events, model.Event{
Day: days[table],
Day: day,
Week: getTextContent(tableData[0]),
Start: start,
End: end,

View File

@ -20,7 +20,9 @@ import (
"fmt"
"github.com/pocketbase/pocketbase/tools/types"
"htwkalender/data-manager/model"
"os"
"reflect"
"sort"
"testing"
"time"
)
@ -588,3 +590,271 @@ func TestIsWinterSemester(t *testing.T) {
})
}
}
func Test_parseSeminarGroup(t *testing.T) {
type args struct {
result string
}
//read string from fil
byteArray, err := os.ReadFile("tests/seminarGroup.html")
if err != nil {
t.Errorf("Error reading file: %v", err)
}
htmlString := string(byteArray)
tests := []struct {
name string
args args
want model.SeminarGroup
}{
{
name: "Test 1",
args: args{
result: htmlString,
},
want: model.SeminarGroup{
Course: "23SAM",
University: "HTWK Leipzig",
Events: []model.Event{
{
UUID: "6ebe83db-f29e-5ddd-ae8f-8724b5ba8959",
Day: "Donnerstag",
Week: "44",
Start: parseDateTime("2024-10-31 06:00:00.000Z"),
End: parseDateTime("2024-10-31 23:00:00.000Z"),
Name: "Feiertage und lehrveranstaltungsfreie Tage",
Notes: "Reformationstag",
Prof: " ",
Rooms: " ",
BookedAt: "30/07/2024",
Course: "23SAM",
EventType: "Sperr",
Compulsory: "",
Semester: "ws",
},
{
UUID: "6ebe83db-f29e-5ddd-ae8f-8724b5ba8959",
Day: "Freitag",
Week: "44",
Start: parseDateTime("2024-11-01 06:00:00.000Z"),
End: parseDateTime("2024-11-01 23:00:00.000Z"),
Name: "Feiertage und lehrveranstaltungsfreie Tage",
Notes: "Brückentag Reformationstag",
Prof: " ",
Rooms: " ",
BookedAt: "30/07/2024",
Course: "23SAM",
EventType: "Sperr",
Compulsory: "",
Semester: "ws",
},
{
UUID: "15e6d285-5ecd-5039-b4b2-d6fcc3dbc1a7",
Day: "Dienstag",
Week: "42",
Start: parseDateTime("2024-10-15 09:15:00.000Z"),
End: parseDateTime("2024-10-15 10:45:00.000Z"),
Name: "3.2 Leitungskompetenzen II SA-M 3. FS (pf)",
Notes: "Leitungshandeln",
Prof: "Prof. Dr. phil. Grit Behse-Bartels",
Rooms: "LI119-S",
BookedAt: "13/06/2024",
Course: "23SAM",
EventType: "S",
Compulsory: "p",
Semester: "ws",
},
{
UUID: "15e6d285-5ecd-5039-b4b2-d6fcc3dbc1a7",
Day: "Dienstag",
Week: "43",
Start: parseDateTime("2024-10-22 09:15:00.000Z"),
End: parseDateTime("2024-10-22 10:45:00.000Z"),
Name: "3.2 Leitungskompetenzen II SA-M 3. FS (pf)",
Notes: "Leitungshandeln",
Prof: "Prof. Dr. phil. Grit Behse-Bartels",
Rooms: "LI119-S",
BookedAt: "13/06/2024",
Course: "23SAM",
EventType: "S",
Compulsory: "p",
Semester: "ws",
},
{
UUID: "15e6d285-5ecd-5039-b4b2-d6fcc3dbc1a7",
Day: "Dienstag",
Week: "44",
Start: parseDateTime("2024-10-29 10:15:00.000Z"),
End: parseDateTime("2024-10-29 11:45:00.000Z"),
Name: "3.2 Leitungskompetenzen II SA-M 3. FS (pf)",
Notes: "Leitungshandeln",
Prof: "Prof. Dr. phil. Grit Behse-Bartels",
Rooms: "LI119-S",
BookedAt: "13/06/2024",
Course: "23SAM",
EventType: "S",
Compulsory: "p",
Semester: "ws",
},
{
UUID: "15e6d285-5ecd-5039-b4b2-d6fcc3dbc1a7",
Day: "Dienstag",
Week: "54",
Start: parseDateTime("2025-01-07 10:15:00.000Z"),
End: parseDateTime("2025-01-07 11:45:00.000Z"),
Name: "3.2 Leitungskompetenzen II SA-M 3. FS (pf)",
Notes: "Leitungshandeln",
Prof: "Prof. Dr. phil. Grit Behse-Bartels",
Rooms: "LI119-S",
BookedAt: "13/06/2024",
Course: "23SAM",
EventType: "S",
Compulsory: "p",
Semester: "ws",
},
{
UUID: "15e6d285-5ecd-5039-b4b2-d6fcc3dbc1a7",
Day: "Dienstag",
Week: "55",
Start: parseDateTime("2025-01-14 10:15:00.000Z"),
End: parseDateTime("2025-01-14 11:45:00.000Z"),
Name: "3.2 Leitungskompetenzen II SA-M 3. FS (pf)",
Notes: "Leitungshandeln",
Prof: "Prof. Dr. phil. Grit Behse-Bartels",
Rooms: "LI119-S",
BookedAt: "13/06/2024",
Course: "23SAM",
EventType: "S",
Compulsory: "p",
Semester: "ws",
},
{
UUID: "6ebe83db-f29e-5ddd-ae8f-8724b5ba8959",
Day: "Mittwoch",
Week: "47",
Start: parseDateTime("2024-11-20 06:00:00.000Z"),
End: parseDateTime("2024-11-20 23:00:00.000Z"),
Name: "Feiertage und lehrveranstaltungsfreie Tage",
Notes: "Buß- und Bettag",
Prof: " ",
Rooms: " ",
BookedAt: "30/07/2024",
Course: "23SAM",
EventType: "Sperr",
Compulsory: "",
Semester: "ws",
},
{
UUID: "703e19b7-06ab-543d-a759-4ef72627594c",
Day: "Mittwoch",
Week: "43",
Start: parseDateTime("2024-10-23 07:30:00.000Z"),
End: parseDateTime("2024-10-23 10:45:00.000Z"),
Name: "3.5 Ausgew. Thema aus dem Thema Fachdiskurs Soz. Arbeit SA-M 3. FS (pf)",
Notes: "LBA Sarah Otto",
Rooms: "LI016-S",
Prof: " ",
BookedAt: "27/08/2024",
Course: "23SAM",
EventType: "S",
Compulsory: "p",
Semester: "ws",
},
{
UUID: "703e19b7-06ab-543d-a759-4ef72627594c",
Day: "Mittwoch",
Week: "46",
Start: parseDateTime("2024-11-13 08:30:00.000Z"),
End: parseDateTime("2024-11-13 11:45:00.000Z"),
Name: "3.5 Ausgew. Thema aus dem Thema Fachdiskurs Soz. Arbeit SA-M 3. FS (pf)",
Notes: "LBA Sarah Otto",
Rooms: "LI115-L",
Prof: " ",
BookedAt: "18/09/2024",
Course: "23SAM",
EventType: "S",
Compulsory: "p",
Semester: "ws",
},
{
UUID: "703e19b7-06ab-543d-a759-4ef72627594c",
Day: "Mittwoch",
Week: "48",
Start: parseDateTime("2024-11-27 08:30:00.000Z"),
End: parseDateTime("2024-11-27 11:45:00.000Z"),
Name: "3.5 Ausgew. Thema aus dem Thema Fachdiskurs Soz. Arbeit SA-M 3. FS (pf)",
Prof: " ",
Notes: "LBA Sarah Otto",
Rooms: "LI201-S",
BookedAt: "18/09/2024",
Course: "23SAM",
EventType: "S",
Compulsory: "p",
Semester: "ws",
},
{
UUID: "02d01aad-1542-574d-b597-aa5ac9ff0179",
Day: "Mittwoch",
Week: "42",
Start: parseDateTime("2024-10-16 11:45:00.000Z"),
End: parseDateTime("2024-10-16 18:30:00.000Z"),
Name: "zentrale Gremienzeit",
Notes: " ",
Prof: " ",
Rooms: " ",
BookedAt: "09/09/2024",
Course: "23SAM",
EventType: "Sperr",
Compulsory: "",
Semester: "ws",
},
{
UUID: "02d01aad-1542-574d-b597-aa5ac9ff0179",
Day: "Mittwoch",
Week: "44",
Start: parseDateTime("2024-10-30 12:45:00.000Z"),
End: parseDateTime("2024-10-30 19:30:00.000Z"),
Name: "zentrale Gremienzeit",
Notes: " ",
Prof: " ",
Rooms: " ",
BookedAt: "09/09/2024",
Course: "23SAM",
EventType: "Sperr",
Compulsory: "",
Semester: "ws",
},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
//sort Arrays by StartDate
sortEventsByStartDate(tt.want.Events)
got := parseSeminarGroup(tt.args.result)
sortEventsByStartDate(got.Events)
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("parseSeminarGroup() = %v, want %v", got, tt.want)
}
})
}
}
func parseDateTime(timeString string) types.DateTime {
dateTime, err := types.ParseDateTime(timeString)
if err != nil {
fmt.Println(err)
}
return dateTime
}
func sortEventsByStartDate(events []model.Event) {
sort.Slice(events, func(i, j int) bool {
return events[i].Start.Time().Before(events[j].Start.Time())
})
}

View File

@ -18,6 +18,7 @@ package v1
import (
"golang.org/x/net/html"
"log/slog"
"strings"
)
@ -39,6 +40,10 @@ func findFirstTable(node *html.Node) *html.Node {
// Find the first <span> element with the specified class attribute value
func findFirstSpanWithClass(node *html.Node, classValue string) *html.Node {
if node == nil {
return nil
}
// Check if the current node is a <span> element with the specified class attribute value
if node.Type == html.ElementNode && node.Data == "span" {
if hasClassAttribute(node, classValue) {
@ -67,19 +72,39 @@ func hasClassAttribute(node *html.Node, classValue string) bool {
}
// Get Tables with days
func getEventTables(node *html.Node) [][]*html.Node {
var eventTables [][]*html.Node
func getEventTables(node *html.Node, dayLabels []string) map[string][]*html.Node {
// Create a map to store the tables with the corresponding day from the dayLabels
dayTablesMap := make(map[string][]*html.Node)
tables := findTables(node)
// get all tables with events
for events := range tables {
rows := findTableRows(tables[events])
// check that a first row exists
if len(rows) > 0 {
rows = rows[1:]
eventTables = append(eventTables, rows)
// Ensure we have the same number of tables as day labels
if len(tables) != len(dayLabels) {
// Handle the case where the number of tables doesn't match the dayLabels (log error or return early)
slog.Error("Number of tables does not match number of day labels")
return dayTablesMap // Returning empty map
}
// Iterate over dayLabels and their corresponding tables
for i, day := range dayLabels {
rows := findTableRows(tables[i])
// check that rows exist and skip the header
if len(rows) > 1 {
rows = rows[1:] // Skip header row
// Add the event rows to the map entry for this day
dayTablesMap[day] = rows
}
}
return eventTables
// Remove days that have no events (empty slices)
for day, eventTable := range dayTablesMap {
if len(eventTable) == 0 {
delete(dayTablesMap, day)
}
}
return dayTablesMap
}
// Get Tables with days

View File

@ -0,0 +1,351 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"><head>
<title>sws_semgrp</title>
<style type="text/css">
body{font: 10pt "Times New Roman",serif; }
span.scientia-footer { font: bold 150% "Times New Roman", serif; color: rgb(255,0,0); }
div.scientia-footer-message { text-align: center }
table.header-0-args{text-align: left;vertical-align: top;font: 9pt "MS Sans Serif",sans-serif; }
table.header-1-args{text-align: left;vertical-align: top;font: 9pt "MS Sans Serif",sans-serif; }
table.header-2-args{text-align: left;vertical-align: top;font: 9pt "MS Sans Serif",sans-serif; }
table.header-3-args{text-align: left;vertical-align: top;font: 9pt "MS Sans Serif",sans-serif; }
table.header-4-args{text-align: left;vertical-align: top;font: 9pt "MS Sans Serif",sans-serif; }
table.header-5-args{text-align: left;vertical-align: top;font: 9pt "MS Sans Serif",sans-serif; }
table.header-border-args{border: 0pt solid rgb(192,192,192); }
span.header-0-1-0{}
span.header-0-2-0{text-align: right;vertical-align: bottom;}
span.header-0-2-1{}
span.header-0-2-2{text-align: right;vertical-align: bottom;}
span.header-0-2-3{text-align: left;vertical-align: top;}
span.header-1-0-0{text-align: left;vertical-align: middle;font: bold 14pt "MS Sans Serif",sans-serif; }
span.header-2-0-0{text-align: center;vertical-align: middle;font: bold 12pt "MS Sans Serif",sans-serif; }
span.header-2-0-1{text-align: center;vertical-align: middle;font: bold 12pt "MS Sans Serif",sans-serif; }
span.header-3-0-0{text-align: center;vertical-align: middle;font: bold 12pt "MS Sans Serif",sans-serif; }
span.header-3-0-1{text-align: center;vertical-align: middle;font: bold 12pt "MS Sans Serif",sans-serif; }
span.header-3-0-2{}
span.header-4-0-0{text-align: left;vertical-align: top;}
span.header-5-0-0{text-align: left;vertical-align: top;font: 8pt "MS Sans Serif",sans-serif; }
span.header-5-0-1{text-align: left;vertical-align: top;}
table.footer-0-args{text-align: left;vertical-align: top;font: 9pt "MS Sans Serif",sans-serif; }
table.footer-1-args{text-align: left;vertical-align: top;font: 9pt "MS Sans Serif",sans-serif; }
table.footer-2-args{text-align: left;vertical-align: top;font: 9pt "MS Sans Serif",sans-serif; }
table.footer-3-args{text-align: left;vertical-align: top;font: 9pt "MS Sans Serif",sans-serif; }
table.footer-border-args{}
span.footer-0-0-0{text-align: left;vertical-align: top;font: bold 9pt "MS Sans Serif",sans-serif; }
span.footer-1-0-0{}
span.footer-2-0-0{text-align: left;vertical-align: top;}
span.footer-3-0-0{text-align: center;vertical-align: middle;font: 8pt "MS Sans Serif",sans-serif; }
span.footer-3-0-1{text-align: center;vertical-align: middle;font: 8pt "MS Sans Serif",sans-serif; }
span.footer-3-0-2{text-align: center;vertical-align: middle;font: 8pt "MS Sans Serif",sans-serif; }
table.spreadsheet {text-align: left;vertical-align: top;font: 9pt "MS Sans Serif",sans-serif; }
tr.columnTitles {text-align: left;vertical-align: top;color: rgb(255,255,255); background-color: rgb(64,64,64); font: bold 9pt "MS Sans Serif",sans-serif; }
col.column0 {text-align: left;vertical-align: middle;}
col.column1 {}
col.column2 {}
col.column3 {text-align: left;vertical-align: top;}
col.column4 {text-align: left;vertical-align: top;}
col.column5 {text-align: left;vertical-align: top;}
col.column6 {text-align: left;vertical-align: top;}
col.column7 {text-align: left;vertical-align: top;}
col.column8 {text-align: center;vertical-align: middle;}
span.labelone{text-align: left;vertical-align: top;font: bold 12pt "MS Sans Serif",sans-serif; }
span.labeltwo{}
</style>
</head>
<body>
<table class="header-border-args" border="0" cellspacing="0" width="100%"><tbody><tr>
<td>
<table cellspacing="0" border="0" width="100%" class="header-0-args">
<colgroup><col align="left"><col align="center"><col align="right">
</colgroup><tbody><tr>
<td></td><td><span class="header-0-1-0"> </span></td><td><span class="header-0-2-0">Wintersemester 2024/25 (Planungszeitraum 01.09.2024 bis 28.02.2025)</span><span class="header-0-2-1">-</span><span class="header-0-2-2">gültig</span><span class="header-0-2-3"></span></td>
</tr>
</tbody></table>
</td>
</tr><tr>
<td>
<table cellspacing="0" border="0" width="100%" class="header-1-args">
<colgroup><col align="left"><col align="center"><col align="right">
</colgroup><tbody><tr>
<td><span class="header-1-0-0">HTWK Leipzig</span></td><td></td><td></td>
</tr>
</tbody></table>
</td>
</tr><tr>
<td>
<table cellspacing="0" border="0" width="100%" class="header-2-args">
<colgroup><col align="left"><col align="center"><col align="right">
</colgroup><tbody><tr>
<td><span class="header-2-0-0">Seminargruppenplan </span><span class="header-2-0-1">23SAM</span></td><td></td><td></td>
</tr>
</tbody></table>
</td>
</tr><tr>
<td>
<table cellspacing="0" border="0" width="100%" class="header-3-args">
<colgroup><col align="left"><col align="center"><col align="right">
</colgroup><tbody><tr>
<td><span class="header-3-0-0">Plannungswoche </span><span class="header-3-0-1">1-65</span><span class="header-3-0-2"><a id="all_weeks" href="." onclick="document.getElementById('all_weeks').setAttribute('href', window.location.href+'&amp;weeks=1-65');"> (alle Wochen anzeigen)</a></span></td><td></td><td></td>
</tr>
</tbody></table>
</td>
</tr><tr>
<td>
<table cellspacing="0" border="0" width="100%" class="header-4-args">
<colgroup><col align="left"><col align="center"><col align="right">
</colgroup><tbody><tr>
<td><span class="header-4-0-0"><style type="text/css">
body{max-width:800pt; font: 10pt "MS Sans Serif",sans-serif;}
table {width: 100%}
table.header-border-args > tbody > tr:nth-child(1) > td > table > tbody > tr > td:nth-child(3) {text-align:right}
table.header-border-args > tbody > tr > td > table > tbody > tr > td {text-align: left}
table.header-border-args > tbody > tr:nth-child(2) > td > table > tbody > tr > td {font-size: 14pt}
table.header-border-args > tbody > tr:nth-child(3) > td > table > tbody > tr > td {font-size: 12pt}
table.header-border-args > tbody > tr:nth-child(4) > td > table > tbody > tr > td {font-size: 12pt}
table.header-border-args > tbody > tr:nth-child(7) > td > table > tbody > tr > td:nth-child(1) {white-space:pre}
tr.columnTitles {text-align: left;vertical-align: top;color: rgb(255,255,255); background-color: rgb(64,64,64); font: bold 9pt "MS Sans Serif",sans-serif; }
table.footer-border-args > tbody > tr:nth-child(1) > td > table {width:100%; max-width:100%}
table.footer-0-args {width: 80%; margin-top:2em; padding:1em; background-color:LightGrey; font-weight: bold; font-size: 10pt; white-space:pre}
</style></span></td><td></td><td></td>
</tr>
</tbody></table>
</td>
</tr><tr>
<td>
<table cellspacing="0" border="0" width="100%" class="header-5-args">
<colgroup><col align="left"><col align="center"><col align="right">
</colgroup><tbody><tr>
<td><span class="header-5-0-0"> </span><span class="header-5-0-1"> </span></td><td></td><td></td>
</tr>
</tbody></table>
</td>
</tr>
</tbody></table>
<p><span class="labelone">Montag</span></p>
<table class="spreadsheet" cellspacing="0" cellpadding="2%" border="t">
<colgroup><col class="column0"><col class="column1"><col class="column2"><col class="column3"><col class="column4"><col class="column5"><col class="column6"><col class="column7"><col class="column8">
</colgroup></table>
<p><span class="labelone">Dienstag</span></p>
<table class="spreadsheet" cellspacing="0" cellpadding="2%" border="t">
<colgroup><col class="column0"><col class="column1"><col class="column2"><col class="column3"><col class="column4"><col class="column5"><col class="column6"><col class="column7"><col class="column8">
</colgroup><tbody><tr class="columnTitles">
<td>Planungswochen</td>
<td>Beginn</td>
<td>Ende</td>
<td>Veranstaltung</td>
<td>Art</td>
<td>Dozent</td>
<td>Räume</td>
<td>Bemerkungen</td>
<td>Gebucht am</td>
</tr>
<tr>
<td>42-44, 54-55</td>
<td>11:15</td>
<td>12:45</td>
<td>3.2 Leitungskompetenzen II SA-M 3. FS (pf)</td>
<td>Sp</td>
<td>Prof. Dr. phil. Grit Behse-Bartels</td>
<td>LI119-S</td>
<td>Leitungshandeln</td>
<td>13/06/2024</td>
</tr>
</tbody></table>
<p><span class="labelone">Mittwoch</span></p>
<table class="spreadsheet" cellspacing="0" cellpadding="2%" border="t">
<colgroup><col class="column0"><col class="column1"><col class="column2"><col class="column3"><col class="column4"><col class="column5"><col class="column6"><col class="column7"><col class="column8">
</colgroup><tbody><tr class="columnTitles">
<td>Planungswochen</td>
<td>Beginn</td>
<td>Ende</td>
<td>Veranstaltung</td>
<td>Art</td>
<td>Dozent</td>
<td>Räume</td>
<td>Bemerkungen</td>
<td>Gebucht am</td>
</tr>
<tr>
<td>47</td>
<td>7:00</td>
<td>0:00</td>
<td>Feiertage und lehrveranstaltungsfreie Tage</td>
<td>Sperr</td>
<td>&nbsp;</td>
<td>&nbsp;</td>
<td>Buß- und Bettag</td>
<td>30/07/2024</td>
</tr>
<tr>
<td>43</td>
<td>9:30</td>
<td>12:45</td>
<td>3.5 Ausgew. Thema aus dem Thema Fachdiskurs Soz. Arbeit SA-M 3. FS (pf)</td>
<td>Sp</td>
<td>&nbsp;</td>
<td>LI016-S</td>
<td>LBA Sarah Otto</td>
<td>27/08/2024</td>
</tr>
<tr>
<td>46</td>
<td>9:30</td>
<td>12:45</td>
<td>3.5 Ausgew. Thema aus dem Thema Fachdiskurs Soz. Arbeit SA-M 3. FS (pf)</td>
<td>Sp</td>
<td>&nbsp;</td>
<td>LI115-L</td>
<td>LBA Sarah Otto</td>
<td>18/09/2024</td>
</tr>
<tr>
<td>48</td>
<td>9:30</td>
<td>12:45</td>
<td>3.5 Ausgew. Thema aus dem Thema Fachdiskurs Soz. Arbeit SA-M 3. FS (pf)</td>
<td>Sp</td>
<td>&nbsp;</td>
<td>LI201-S</td>
<td>LBA Sarah Otto</td>
<td>18/09/2024</td>
</tr>
<tr>
<td>42, 44</td>
<td>13:45</td>
<td>20:30</td>
<td>zentrale Gremienzeit</td>
<td>Sperr</td>
<td>&nbsp;</td>
<td>&nbsp;</td>
<td>&nbsp;</td>
<td>09/09/2024</td>
</tr>
</tbody></table>
<p><span class="labelone">Donnerstag</span></p>
<table class="spreadsheet" cellspacing="0" cellpadding="2%" border="t">
<colgroup><col class="column0"><col class="column1"><col class="column2"><col class="column3"><col class="column4"><col class="column5"><col class="column6"><col class="column7"><col class="column8">
</colgroup><tbody><tr class="columnTitles">
<td>Planungswochen</td>
<td>Beginn</td>
<td>Ende</td>
<td>Veranstaltung</td>
<td>Art</td>
<td>Dozent</td>
<td>Räume</td>
<td>Bemerkungen</td>
<td>Gebucht am</td>
</tr>
<tr>
<td>44</td>
<td>7:00</td>
<td>0:00</td>
<td>Feiertage und lehrveranstaltungsfreie Tage</td>
<td>Sperr</td>
<td>&nbsp;</td>
<td>&nbsp;</td>
<td>Reformationstag</td>
<td>30/07/2024</td>
</tr>
</tbody></table>
<p><span class="labelone">Freitag</span></p>
<table class="spreadsheet" cellspacing="0" cellpadding="2%" border="t">
<colgroup><col class="column0"><col class="column1"><col class="column2"><col class="column3"><col class="column4"><col class="column5"><col class="column6"><col class="column7"><col class="column8">
</colgroup><tbody><tr class="columnTitles">
<td>Planungswochen</td>
<td>Beginn</td>
<td>Ende</td>
<td>Veranstaltung</td>
<td>Art</td>
<td>Dozent</td>
<td>Räume</td>
<td>Bemerkungen</td>
<td>Gebucht am</td>
</tr>
<tr>
<td>44</td>
<td>7:00</td>
<td>0:00</td>
<td>Feiertage und lehrveranstaltungsfreie Tage</td>
<td>Sperr</td>
<td>&nbsp;</td>
<td>&nbsp;</td>
<td>Brückentag Reformationstag</td>
<td>30/07/2024</td>
</tr>
</tbody></table>
<p><span class="labelone">Samstag</span></p>
<table class="spreadsheet" cellspacing="0" cellpadding="2%" border="t">
<colgroup><col class="column0"><col class="column1"><col class="column2"><col class="column3"><col class="column4"><col class="column5"><col class="column6"><col class="column7"><col class="column8">
</colgroup></table>
<p><span class="labelone">Sonntag</span></p>
<table class="spreadsheet" cellspacing="0" cellpadding="2%" border="t">
<colgroup><col class="column0"><col class="column1"><col class="column2"><col class="column3"><col class="column4"><col class="column5"><col class="column6"><col class="column7"><col class="column8">
</colgroup></table>
<table class="footer-border-args" border="0" cellspacing="0" width="100%"><tbody><tr>
<td>
<table cellspacing="0" border="0" width="100%" class="footer-0-args">
<colgroup><col align="left"><col align="center"><col align="right">
</colgroup><tbody><tr>
<td><span class="footer-0-0-0">Doppellehrveranstaltungen können je nach Beginn 30 min Frühstückspause oder 60 min Mittagspause enthalten.</span></td><td></td><td></td>
</tr>
</tbody></table>
</td>
</tr><tr>
<td>
<table cellspacing="0" border="0" width="100%" class="footer-1-args">
<colgroup><col align="left"><col align="center"><col align="right">
</colgroup><tbody><tr>
<td><span class="footer-1-0-0"> </span></td><td></td><td></td>
</tr>
</tbody></table>
</td>
</tr><tr>
<td>
<table cellspacing="0" border="0" width="100%" class="footer-2-args">
<colgroup><col align="left"><col align="center"><col align="right">
</colgroup><tbody><tr>
<td><span class="footer-2-0-0"><p style="font-size: 11pt">
<b>Wichtige Hinweise:</b>
</p><ul style="font-size: 10pt">
<li><a href="http://www.htwk-leipzig.de/lageplan" target="_blank"><b>Campusplan</b></a> (mit Gebäudeabkürzungen unter www.htwk-leipzig.de/lageplan)</li>
<li><a href="http://www.htwk-leipzig.de/telefonverzeichnis" target="_blank"><b>Personalverzeichnis</b></a> (mit Funktion, E-Mail, Telefon, Raum etc. unter www.htwk-leipzig.de/telefonverzeichnis)</li>
<li><a href="http://www.htwk-leipzig.de/akademischer-Kalender" target="_blank"><b>Akademischer Kalender</b></a> (mit Terminen und Planungwochenübersicht unter www.htwk-leipzig.de/akademischer-Kalender)</li>
<li><a href="http://www.htwk-leipzig.de/lvp-hilfe-student" target="_blank"><b>Hilfe für Studierende</b></a> (mit Anleitungen und Abkürzungen unter www.htwk-leipzig.de/lvp-hilfe-student)</li>
<li><a href="http://www.htwk-leipzig.de/lvp-hilfe-dozent" target="_blank"><b>Hilfe für Dozenten</b></a> (mit Abkürzungen und Hinweisen zu Raumbuchung/Zutritt unter www.htwk-leipzig.de/lvp-hilfe-dozent)</li>
</ul>
<p></p></span></td><td></td><td></td>
</tr>
</tbody></table>
</td>
</tr><tr>
<td>
<table cellspacing="0" border="0" width="100%" class="footer-3-args">
<colgroup><col align="left"><col align="center"><col align="right">
</colgroup><tbody><tr>
<td><span class="footer-3-0-0">07/10/2024</span><span class="footer-3-0-1"> </span><span class="footer-3-0-2">15:08</span></td><td></td><td></td>
</tr>
</tbody></table>
</td>
</tr>
</tbody></table>
</body></html>

View File

@ -2,8 +2,10 @@ package grpc
import (
"github.com/pocketbase/pocketbase"
"google.golang.org/grpc/keepalive"
"log"
"net"
"time"
"google.golang.org/grpc"
pb "htwkalender/common/genproto/modules"
@ -14,7 +16,20 @@ func StartGRPCServer(app *pocketbase.PocketBase) {
if err != nil {
log.Fatalf("failed to listen: %v", err)
}
s := grpc.NewServer()
s := grpc.NewServer(
grpc.KeepaliveParams(keepalive.ServerParameters{
MaxConnectionIdle: 5 * time.Minute, // Idle timeout before closing connection
MaxConnectionAge: 30 * time.Minute, // Max time before connection is closed
MaxConnectionAgeGrace: 5 * time.Minute, // Allow grace period before closing
Time: 2 * time.Minute, // Ping the client every 2 minutes
Timeout: 20 * time.Second, // Wait 20 seconds for ping ack
}),
grpc.KeepaliveEnforcementPolicy(keepalive.EnforcementPolicy{
MinTime: 1 * time.Minute, // Minimum time between pings from clients
PermitWithoutStream: true, // Don't allow pings when there are no active RPCs
}),
grpc.MaxConcurrentStreams(0),
)
pb.RegisterModuleServiceServer(s, &ModuleServiceHandler{
app: app,

View File

@ -39,8 +39,15 @@ func main() {
}
grpcClient := grpc.ConnectGRPCServer(host)
// Close the grpc connection when the main function ends
defer grpc.CloseGRPCServer(grpcClient)
// Log the grpc connection
// Test the connection to the grpc server
grpcClient.Connect()
slog.Info("GRPC connection state", "state", grpcClient.GetState())
// Initialize a new Fiber app
webdavRequestMethods := []string{"PROPFIND", "MKCOL", "COPY", "MOVE"}
@ -64,7 +71,7 @@ func main() {
fiberApp.Use(logger.New(
logger.Config{
Format: "[${time}] ${status} - ${latency} ${method} ${path} ${error}\n",
Format: "${time} | ${status} | ${latency} | ${method} - ${path} | ${error}\n",
TimeFormat: "02-01-2006 15:04:05",
},
))

View File

@ -37,6 +37,14 @@ func (events Events) Sort() {
})
}
func (events Events) String() string {
var str strings.Builder
for _, event := range events {
str.WriteString(event.String())
}
return str.String()
}
type AnonymizedEventDTO struct {
Day string `db:"Day" json:"day"`
Week string `db:"Week" json:"week"`
@ -109,3 +117,7 @@ func (e *Event) GetName() string {
func (e *Event) SetName(name string) {
e.Name = name
}
func (e *Event) String() string {
return e.UUID + e.Day + e.Week + e.Start.String() + e.End.String() + e.Name + e.EventType + e.Compulsory + e.Prof + e.Rooms + e.Notes + e.BookedAt + e.Course + e.Semester
}

View File

@ -92,3 +92,7 @@ func ToJSONTime(timeString string) JSONTime {
}
return JSONTime(t)
}
func (j JSONTime) String() string {
return time.Time(j).Format(DefaultDateLayout)
}

View File

@ -19,14 +19,31 @@ package grpc
import (
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
"google.golang.org/grpc/keepalive"
"log/slog"
"sync"
"time"
)
var conn *grpc.ClientConn
var once sync.Once
func ConnectGRPCServer(host string) *grpc.ClientConn {
conn, err := grpc.NewClient(host+":50051", grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
slog.Error("could not connect to grpc server", "error", err)
}
once.Do(func() {
var err error
conn, err = grpc.NewClient(
host+":50051",
grpc.WithTransportCredentials(insecure.NewCredentials()),
grpc.WithKeepaliveParams(keepalive.ClientParameters{
Time: 2 * time.Minute,
Timeout: 20 * time.Second,
PermitWithoutStream: true,
}),
)
if err != nil {
slog.Error("could not connect to grpc server", "error", err)
}
})
return conn
}

View File

@ -21,6 +21,7 @@ import (
"htwkalender/ical/model"
"htwkalender/ical/service/connector"
htwkalenderGrpc "htwkalender/ical/service/connector/grpc"
"htwkalender/ical/service/functions"
"log/slog"
"time"
)
@ -29,7 +30,7 @@ const expirationTime = 5 * time.Minute
var FeedDeletedError = fmt.Errorf("feed deleted")
func Feed(app model.AppType, token string, userAgent string) (string, error) {
func Feed(app model.AppType, token string, userAgent string) (string, string, error) {
var events model.Events
modules := map[string]model.FeedCollection{}
@ -45,17 +46,17 @@ func Feed(app model.AppType, token string, userAgent string) (string, error) {
// get feed by token
feed, err := htwkalenderGrpc.GetFeed(token, app.GrpcClient)
if err != nil {
return "", err
return "", "", err
}
if feed.Deleted {
return "", FeedDeletedError
return "", "", FeedDeletedError
}
// Get all events for modules
events, err = htwkalenderGrpc.GetEvents(feed.Modules, app.GrpcClient)
if err != nil {
return "", err
return "", "", err
}
// Sort events by start date
@ -66,10 +67,13 @@ func Feed(app model.AppType, token string, userAgent string) (string, error) {
}
}
// Generate one Hash for E-TAG from all events and modules
etag := functions.HashString(events.String() + fmt.Sprint(modules))
cal := GenerateIcalFeed(events, modules, userAgent)
icalFeed := &model.FeedModel{Content: cal.Serialize(), ExpiresAt: model.JSONTime(time.Now().Add(expirationTime))}
return icalFeed.Content, nil
return icalFeed.Content, etag, nil
}
func FeedRecord(app model.AppType, token string) (model.FeedRecord, error) {

View File

@ -34,13 +34,12 @@ func AddFeedRoutes(app model.AppType) {
app.Fiber.Get("/api/feed", func(c fiber.Ctx) error {
token := c.Query("token")
ifNoneMatch := c.Get("If-None-Match")
// get request userAgent and check if it is Thunderbird
userAgent := c.Get("User-Agent")
slog.Info("User-Agent", "userAgent", userAgent)
results, err := ical.Feed(app, token, userAgent)
results, eTag, err := ical.Feed(app, token, userAgent)
if errors.Is(err, ical.FeedDeletedError) {
return c.SendStatus(fiber.StatusGone)
@ -50,10 +49,16 @@ func AddFeedRoutes(app model.AppType) {
slog.Error("Failed to get feed", "error", err, "token", token)
return c.SendStatus(fiber.StatusNotFound)
}
if ifNoneMatch == eTag && ifNoneMatch != "" {
return c.SendStatus(fiber.StatusNotModified)
}
c.Response().Header.Set("Content-type", "text/calendar")
c.Response().Header.Set("charset", "utf-8")
c.Response().Header.Set("Content-Disposition", "inline")
c.Response().Header.Set("filename", "calendar.ics")
c.Response().Header.Set("ETag", eTag)
return c.SendString(results)
})