mirror of
https://gitlab.dit.htwk-leipzig.de/htwk-software/htwkalender.git
synced 2025-07-16 09:38:49 +02:00
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:
@ -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
|
||||
|
@ -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
|
||||
|
@ -35,6 +35,8 @@ services:
|
||||
target: dev # prod
|
||||
environment:
|
||||
- DATA_MANAGER_URL=htwkalender-data-manager
|
||||
depends_on:
|
||||
- htwkalender-data-manager
|
||||
|
||||
htwkalender-frontend:
|
||||
build:
|
||||
|
@ -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;
|
||||
|
@ -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!",
|
||||
|
@ -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!",
|
||||
|
@ -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>
|
||||
|
||||
|
@ -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')"
|
||||
>
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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,
|
||||
|
@ -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())
|
||||
})
|
||||
}
|
||||
|
@ -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
|
||||
|
351
services/data-manager/service/fetch/v1/tests/seminarGroup.html
Normal file
351
services/data-manager/service/fetch/v1/tests/seminarGroup.html
Normal 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+'&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> </td>
|
||||
<td> </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> </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> </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> </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> </td>
|
||||
<td> </td>
|
||||
<td> </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> </td>
|
||||
<td> </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> </td>
|
||||
<td> </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>
|
@ -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,
|
||||
|
@ -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",
|
||||
},
|
||||
))
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -92,3 +92,7 @@ func ToJSONTime(timeString string) JSONTime {
|
||||
}
|
||||
return JSONTime(t)
|
||||
}
|
||||
|
||||
func (j JSONTime) String() string {
|
||||
return time.Time(j).Format(DefaultDateLayout)
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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) {
|
||||
|
@ -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)
|
||||
})
|
||||
|
Reference in New Issue
Block a user