@@ -58,6 +59,16 @@ along with this program. If not, see
.
+
+ {{ $t("faqView.thirdAnswer.htwk_app.description") }}
+
+ - {{ $t("faqView.thirdAnswer.htwk_app.one") }}
+ - {{ $t("faqView.thirdAnswer.htwk_app.two") }}
+ - {{ $t("faqView.thirdAnswer.htwk_app.three") }}
+ - {{ $t("faqView.thirdAnswer.htwk_app.four") }}
+ - {{ $t("faqView.thirdAnswer.htwk_app.five") }}
+
+
diff --git a/reverseproxy.conf b/reverseproxy.conf
index 55220e6..71332dc 100644
--- a/reverseproxy.conf
+++ b/reverseproxy.conf
@@ -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;
diff --git a/reverseproxy.dev.conf b/reverseproxy.dev.conf
index 793ce4e..d735cad 100644
--- a/reverseproxy.dev.conf
+++ b/reverseproxy.dev.conf
@@ -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;
diff --git a/reverseproxy.local.conf b/reverseproxy.local.conf
index 60b3822..42a12cf 100644
--- a/reverseproxy.local.conf
+++ b/reverseproxy.local.conf
@@ -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;
diff --git a/services/data-manager/service/addRoute.go b/services/data-manager/service/addRoute.go
index 25aa462..562b45d 100644
--- a/services/data-manager/service/addRoute.go
+++ b/services/data-manager/service/addRoute.go
@@ -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,
diff --git a/services/data-manager/service/db/dbGroups.go b/services/data-manager/service/db/dbGroups.go
index 5e70e21..eef2641 100644
--- a/services/data-manager/service/db/dbGroups.go
+++ b/services/data-manager/service/db/dbGroups.go
@@ -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
+}
diff --git a/services/data-manager/service/events/courseService.go b/services/data-manager/service/events/courseService.go
index 65cc1a8..3bf460e 100644
--- a/services/data-manager/service/events/courseService.go
+++ b/services/data-manager/service/events/courseService.go
@@ -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)
+}
diff --git a/services/data-manager/service/events/mock/courseMock.go b/services/data-manager/service/events/mock/courseMock.go
index 80aabce..21b9b8e 100644
--- a/services/data-manager/service/events/mock/courseMock.go
+++ b/services/data-manager/service/events/mock/courseMock.go
@@ -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)
diff --git a/services/data-manager/service/fetch/v1/fetchSeminarEventService.go b/services/data-manager/service/fetch/v1/fetchSeminarEventService.go
index 4a02776..9a0aa1a 100644
--- a/services/data-manager/service/fetch/v1/fetchSeminarEventService.go
+++ b/services/data-manager/service/fetch/v1/fetchSeminarEventService.go
@@ -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,
diff --git a/services/data-manager/service/fetch/v1/fetchSeminarEventService_test.go b/services/data-manager/service/fetch/v1/fetchSeminarEventService_test.go
index f625705..2f0677f 100644
--- a/services/data-manager/service/fetch/v1/fetchSeminarEventService_test.go
+++ b/services/data-manager/service/fetch/v1/fetchSeminarEventService_test.go
@@ -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())
+ })
+}
diff --git a/services/data-manager/service/fetch/v1/htmlParsingFunctions.go b/services/data-manager/service/fetch/v1/htmlParsingFunctions.go
index cf795f2..1268810 100644
--- a/services/data-manager/service/fetch/v1/htmlParsingFunctions.go
+++ b/services/data-manager/service/fetch/v1/htmlParsingFunctions.go
@@ -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 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 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
diff --git a/services/data-manager/service/fetch/v1/tests/seminarGroup.html b/services/data-manager/service/fetch/v1/tests/seminarGroup.html
new file mode 100644
index 0000000..8380d74
--- /dev/null
+++ b/services/data-manager/service/fetch/v1/tests/seminarGroup.html
@@ -0,0 +1,351 @@
+
+
+ sws_semgrp
+
+
+
+
+
+
+Montag
+
+
+Dienstag
+
+
+
+ Planungswochen |
+ Beginn |
+ Ende |
+ Veranstaltung |
+ Art |
+ Dozent |
+ Räume |
+ Bemerkungen |
+ Gebucht am |
+
+
+
+ 42-44, 54-55 |
+ 11:15 |
+ 12:45 |
+ 3.2 Leitungskompetenzen II SA-M 3. FS (pf) |
+ Sp |
+ Prof. Dr. phil. Grit Behse-Bartels |
+ LI119-S |
+ Leitungshandeln |
+ 13/06/2024 |
+
+
+
+Mittwoch
+
+
+
+ Planungswochen |
+ Beginn |
+ Ende |
+ Veranstaltung |
+ Art |
+ Dozent |
+ Räume |
+ Bemerkungen |
+ Gebucht am |
+
+
+ 47 |
+ 7:00 |
+ 0:00 |
+ Feiertage und lehrveranstaltungsfreie Tage |
+ Sperr |
+ |
+ |
+ Buß- und Bettag |
+ 30/07/2024 |
+
+
+
+ 43 |
+ 9:30 |
+ 12:45 |
+ 3.5 Ausgew. Thema aus dem Thema Fachdiskurs Soz. Arbeit SA-M 3. FS (pf) |
+ Sp |
+ |
+ LI016-S |
+ LBA Sarah Otto |
+ 27/08/2024 |
+
+
+
+ 46 |
+ 9:30 |
+ 12:45 |
+ 3.5 Ausgew. Thema aus dem Thema Fachdiskurs Soz. Arbeit SA-M 3. FS (pf) |
+ Sp |
+ |
+ LI115-L |
+ LBA Sarah Otto |
+ 18/09/2024 |
+
+
+ 48 |
+ 9:30 |
+ 12:45 |
+ 3.5 Ausgew. Thema aus dem Thema Fachdiskurs Soz. Arbeit SA-M 3. FS (pf) |
+ Sp |
+ |
+ LI201-S |
+ LBA Sarah Otto |
+ 18/09/2024 |
+
+
+ 42, 44 |
+ 13:45 |
+ 20:30 |
+ zentrale Gremienzeit |
+ Sperr |
+ |
+ |
+ |
+ 09/09/2024 |
+
+
+
+
+Donnerstag
+
+
+
+ Planungswochen |
+ Beginn |
+ Ende |
+ Veranstaltung |
+ Art |
+ Dozent |
+ Räume |
+ Bemerkungen |
+ Gebucht am |
+
+
+ 44 |
+ 7:00 |
+ 0:00 |
+ Feiertage und lehrveranstaltungsfreie Tage |
+ Sperr |
+ |
+ |
+ Reformationstag |
+ 30/07/2024 |
+
+
+
+
+
+
+Freitag
+
+
+
+ Planungswochen |
+ Beginn |
+ Ende |
+ Veranstaltung |
+ Art |
+ Dozent |
+ Räume |
+ Bemerkungen |
+ Gebucht am |
+
+
+ 44 |
+ 7:00 |
+ 0:00 |
+ Feiertage und lehrveranstaltungsfreie Tage |
+ Sperr |
+ |
+ |
+ Brückentag Reformationstag |
+ 30/07/2024 |
+
+
+
+
+Samstag
+
+
+Sonntag
+
+
+
+
+
\ No newline at end of file
diff --git a/services/data-manager/service/grpc/server.go b/services/data-manager/service/grpc/server.go
index f5b6ad3..9992ec0 100644
--- a/services/data-manager/service/grpc/server.go
+++ b/services/data-manager/service/grpc/server.go
@@ -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,
diff --git a/services/ical/main.go b/services/ical/main.go
index 0b8b19f..adff01b 100644
--- a/services/ical/main.go
+++ b/services/ical/main.go
@@ -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",
},
))
diff --git a/services/ical/model/eventModel.go b/services/ical/model/eventModel.go
index 2d92a0d..511ad22 100644
--- a/services/ical/model/eventModel.go
+++ b/services/ical/model/eventModel.go
@@ -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
+}
diff --git a/services/ical/model/icalModel.go b/services/ical/model/icalModel.go
index 14121fd..e5cab49 100644
--- a/services/ical/model/icalModel.go
+++ b/services/ical/model/icalModel.go
@@ -92,3 +92,7 @@ func ToJSONTime(timeString string) JSONTime {
}
return JSONTime(t)
}
+
+func (j JSONTime) String() string {
+ return time.Time(j).Format(DefaultDateLayout)
+}
diff --git a/services/ical/service/connector/grpc/client.go b/services/ical/service/connector/grpc/client.go
index 7b3b261..1954523 100644
--- a/services/ical/service/connector/grpc/client.go
+++ b/services/ical/service/connector/grpc/client.go
@@ -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
}
diff --git a/services/ical/service/ical/ical.go b/services/ical/service/ical/ical.go
index 6a331d5..95f2272 100644
--- a/services/ical/service/ical/ical.go
+++ b/services/ical/service/ical/ical.go
@@ -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) {
diff --git a/services/ical/service/routes.go b/services/ical/service/routes.go
index 09ffc9e..3174d46 100644
--- a/services/ical/service/routes.go
+++ b/services/ical/service/routes.go
@@ -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)
})