mirror of
https://gitlab.dit.htwk-leipzig.de/htwk-software/htwkalender.git
synced 2025-08-03 18:29:14 +02:00
feat:#34 refactored function to intended service, fixed docker files
This commit is contained in:
78
services/data-manager/service/addCalDavRoutes.go
Normal file
78
services/data-manager/service/addCalDavRoutes.go
Normal file
@@ -0,0 +1,78 @@
|
||||
//Calendar implementation for the HTWK Leipzig timetable. Evaluation and display of the individual dates in iCal format.
|
||||
//Copyright (C) 2024 HTWKalender support@htwkalender.de
|
||||
|
||||
//This program is free software: you can redistribute it and/or modify
|
||||
//it under the terms of the GNU Affero General Public License as published by
|
||||
//the Free Software Foundation, either version 3 of the License, or
|
||||
//(at your option) any later version.
|
||||
|
||||
//This program is distributed in the hope that it will be useful,
|
||||
//but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
//MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
//GNU Affero General Public License for more details.
|
||||
|
||||
//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/>.
|
||||
|
||||
package service
|
||||
|
||||
import (
|
||||
"github.com/labstack/echo/v5"
|
||||
"github.com/pocketbase/pocketbase"
|
||||
"github.com/pocketbase/pocketbase/apis"
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
"htwkalender/service/db"
|
||||
"htwkalender/service/ical"
|
||||
"io"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func addFeedRoutes(app *pocketbase.PocketBase) {
|
||||
app.OnBeforeServe().Add(func(e *core.ServeEvent) error {
|
||||
_, err := e.Router.AddRoute(echo.Route{
|
||||
Method: http.MethodPost,
|
||||
Path: "/api/feed",
|
||||
Handler: func(c echo.Context) error {
|
||||
requestBody, _ := io.ReadAll(c.Request().Body)
|
||||
result, err := ical.CreateIndividualFeed(requestBody, app)
|
||||
if err != nil {
|
||||
slog.Error("Failed to create individual feed", "error", err)
|
||||
return c.JSON(http.StatusInternalServerError, "Failed to create individual feed")
|
||||
}
|
||||
return c.JSON(http.StatusOK, result)
|
||||
},
|
||||
Middlewares: []echo.MiddlewareFunc{
|
||||
apis.ActivityLogger(app),
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
app.OnBeforeServe().Add(func(e *core.ServeEvent) error {
|
||||
_, err := e.Router.AddRoute(echo.Route{
|
||||
Method: http.MethodDelete,
|
||||
Path: "/api/feed",
|
||||
Handler: func(c echo.Context) error {
|
||||
token := c.QueryParam("token")
|
||||
err := db.DeleteFeed(app.Dao(), token)
|
||||
if err != nil {
|
||||
return c.JSON(http.StatusNotFound, err)
|
||||
} else {
|
||||
return c.JSON(http.StatusOK, "Feed deleted")
|
||||
}
|
||||
},
|
||||
Middlewares: []echo.MiddlewareFunc{
|
||||
apis.ActivityLogger(app),
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
}
|
430
services/data-manager/service/addRoute.go
Normal file
430
services/data-manager/service/addRoute.go
Normal file
@@ -0,0 +1,430 @@
|
||||
//Calendar implementation for the HTWK Leipzig timetable. Evaluation and display of the individual dates in iCal format.
|
||||
//Copyright (C) 2024 HTWKalender support@htwkalender.de
|
||||
|
||||
//This program is free software: you can redistribute it and/or modify
|
||||
//it under the terms of the GNU Affero General Public License as published by
|
||||
//the Free Software Foundation, either version 3 of the License, or
|
||||
//(at your option) any later version.
|
||||
|
||||
//This program is distributed in the hope that it will be useful,
|
||||
//but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
//MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
//GNU Affero General Public License for more details.
|
||||
|
||||
//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/>.
|
||||
|
||||
package service
|
||||
|
||||
import (
|
||||
"htwkalender/service/course"
|
||||
"htwkalender/service/events"
|
||||
"htwkalender/service/fetch/sport"
|
||||
v1 "htwkalender/service/fetch/v1"
|
||||
v2 "htwkalender/service/fetch/v2"
|
||||
"htwkalender/service/functions/time"
|
||||
"htwkalender/service/room"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
|
||||
"github.com/labstack/echo/v5"
|
||||
"github.com/pocketbase/pocketbase"
|
||||
"github.com/pocketbase/pocketbase/apis"
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
)
|
||||
|
||||
func AddRoutes(app *pocketbase.PocketBase) {
|
||||
|
||||
app.OnBeforeServe().Add(func(e *core.ServeEvent) error {
|
||||
_, err := e.Router.AddRoute(echo.Route{
|
||||
Method: http.MethodGet,
|
||||
Path: "/api/fetch/events",
|
||||
Handler: func(c echo.Context) error {
|
||||
savedEvents, err := v2.ParseEventsFromRemote(app)
|
||||
if err != nil {
|
||||
slog.Error("Failed to parse events from remote: ", "error", err)
|
||||
return c.JSON(http.StatusBadRequest, "Failed to parse events from remote")
|
||||
} else {
|
||||
return c.JSON(http.StatusOK, savedEvents)
|
||||
}
|
||||
},
|
||||
Middlewares: []echo.MiddlewareFunc{
|
||||
apis.ActivityLogger(app),
|
||||
apis.RequireAdminAuth(),
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
app.OnBeforeServe().Add(func(e *core.ServeEvent) error {
|
||||
_, err := e.Router.AddRoute(echo.Route{
|
||||
Method: http.MethodGet,
|
||||
Path: "/api/fetch/daily/events",
|
||||
Handler: func(c echo.Context) error {
|
||||
course.UpdateCourse(app)
|
||||
return c.JSON(http.StatusOK, "Daily events fetched")
|
||||
},
|
||||
Middlewares: []echo.MiddlewareFunc{
|
||||
apis.ActivityLogger(app),
|
||||
apis.RequireAdminAuth(),
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
app.OnBeforeServe().Add(func(e *core.ServeEvent) error {
|
||||
_, err := e.Router.AddRoute(echo.Route{
|
||||
Method: http.MethodGet,
|
||||
Path: "/api/fetch/groups",
|
||||
Handler: func(c echo.Context) error {
|
||||
groups, err := v1.FetchSeminarGroups(app)
|
||||
if err != nil {
|
||||
return c.JSON(http.StatusBadRequest, "Failed to fetch seminar groups")
|
||||
}
|
||||
return c.JSON(http.StatusOK, groups)
|
||||
},
|
||||
Middlewares: []echo.MiddlewareFunc{
|
||||
apis.ActivityLogger(app),
|
||||
apis.RequireAdminAuth(),
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
app.OnBeforeServe().Add(func(e *core.ServeEvent) error {
|
||||
_, err := e.Router.AddRoute(echo.Route{
|
||||
Method: http.MethodGet,
|
||||
Path: "/api/fetch/sports",
|
||||
Handler: func(c echo.Context) error {
|
||||
|
||||
sportEvents, err := sport.FetchAndUpdateSportEvents(app)
|
||||
if err != nil {
|
||||
return c.JSON(http.StatusBadRequest, "Failed to fetch sport events")
|
||||
}
|
||||
return c.JSON(http.StatusOK, sportEvents)
|
||||
},
|
||||
Middlewares: []echo.MiddlewareFunc{
|
||||
apis.ActivityLogger(app),
|
||||
apis.RequireAdminAuth(),
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
app.OnBeforeServe().Add(func(e *core.ServeEvent) error {
|
||||
_, err := e.Router.AddRoute(echo.Route{
|
||||
Method: http.MethodDelete,
|
||||
Path: "/api/modules",
|
||||
Handler: func(c echo.Context) error {
|
||||
err := events.DeleteAllEvents(app)
|
||||
if err != nil {
|
||||
return c.JSON(http.StatusBadRequest, "Failed to delete events")
|
||||
}
|
||||
return c.JSON(http.StatusOK, "Events deleted")
|
||||
},
|
||||
Middlewares: []echo.MiddlewareFunc{
|
||||
apis.ActivityLogger(app),
|
||||
apis.RequireAdminAuth(),
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
app.OnBeforeServe().Add(func(e *core.ServeEvent) error {
|
||||
_, err := e.Router.AddRoute(echo.Route{
|
||||
Method: http.MethodGet,
|
||||
Path: "/api/rooms",
|
||||
Handler: func(c echo.Context) error {
|
||||
rooms, err := room.GetRooms(app)
|
||||
if err != nil {
|
||||
return c.JSON(http.StatusBadRequest, "Failed to get rooms")
|
||||
}
|
||||
return c.JSON(http.StatusOK, rooms)
|
||||
},
|
||||
Middlewares: []echo.MiddlewareFunc{
|
||||
apis.ActivityLogger(app),
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
// API Endpoint to get all events for a specific room on a specific day
|
||||
app.OnBeforeServe().Add(func(e *core.ServeEvent) error {
|
||||
_, err := e.Router.AddRoute(echo.Route{
|
||||
Method: http.MethodGet,
|
||||
Path: "/api/schedule/day",
|
||||
Handler: func(c echo.Context) error {
|
||||
roomParam := c.QueryParam("room")
|
||||
date := c.QueryParam("date")
|
||||
roomSchedule, err := room.GetRoomScheduleForDay(app, roomParam, date)
|
||||
if err != nil {
|
||||
slog.Error("Failed to get room schedule for day: ", "error", err)
|
||||
return c.JSON(http.StatusBadRequest, "Failed to get room schedule for day")
|
||||
}
|
||||
return c.JSON(http.StatusOK, roomSchedule)
|
||||
},
|
||||
Middlewares: []echo.MiddlewareFunc{
|
||||
apis.ActivityLogger(app),
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
// API Endpoint to create a new iCal feed
|
||||
app.OnBeforeServe().Add(func(e *core.ServeEvent) error {
|
||||
_, err := e.Router.AddRoute(echo.Route{
|
||||
Method: http.MethodGet,
|
||||
Path: "/api/schedule",
|
||||
Handler: func(c echo.Context) error {
|
||||
roomParam := c.QueryParam("room")
|
||||
to := c.QueryParam("to")
|
||||
from := c.QueryParam("from")
|
||||
roomSchedule, err := room.GetRoomSchedule(app, roomParam, from, to)
|
||||
if err != nil {
|
||||
slog.Error("Failed to get room schedule:", "error", err)
|
||||
return c.JSON(http.StatusBadRequest, "Failed to get room schedule")
|
||||
}
|
||||
return c.JSON(http.StatusOK, roomSchedule)
|
||||
},
|
||||
Middlewares: []echo.MiddlewareFunc{
|
||||
apis.ActivityLogger(app),
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
// API Endpoint to get all rooms that have no events in a specific time frame
|
||||
app.OnBeforeServe().Add(func(e *core.ServeEvent) error {
|
||||
_, err := e.Router.AddRoute(echo.Route{
|
||||
Method: http.MethodGet,
|
||||
Path: "/api/rooms/free",
|
||||
Handler: func(c echo.Context) error {
|
||||
from, err := time.ParseTime(c.QueryParam("from"))
|
||||
if err != nil {
|
||||
slog.Error("Failed to parse time: ", "error", err)
|
||||
return c.JSON(http.StatusBadRequest, "Failed to parse time")
|
||||
}
|
||||
to, err := time.ParseTime(c.QueryParam("to"))
|
||||
if err != nil {
|
||||
slog.Error("Failed to parse time: ", "error", err)
|
||||
return c.JSON(http.StatusBadRequest, "Failed to parse time")
|
||||
}
|
||||
rooms, err := room.GetFreeRooms(app, from, to)
|
||||
if err != nil {
|
||||
slog.Error("Failed to get free rooms: ", "error", err)
|
||||
return c.JSON(http.StatusBadRequest, "Failed to get free rooms")
|
||||
}
|
||||
return c.JSON(http.StatusOK, rooms)
|
||||
},
|
||||
Middlewares: []echo.MiddlewareFunc{
|
||||
apis.ActivityLogger(app),
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
addFeedRoutes(app)
|
||||
|
||||
app.OnBeforeServe().Add(func(e *core.ServeEvent) error {
|
||||
_, err := e.Router.AddRoute(echo.Route{
|
||||
Method: http.MethodGet,
|
||||
Path: "/api/course/modules",
|
||||
Handler: func(c echo.Context) error {
|
||||
modules, err := events.GetModulesForCourseDistinct(
|
||||
app,
|
||||
c.QueryParam("course"),
|
||||
c.QueryParam("semester"),
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
slog.Error("Failed to get modules for course and semester: ", "error", err)
|
||||
return c.JSON(http.StatusBadRequest, "Failed to get modules for course and semester")
|
||||
} else {
|
||||
return c.JSON(http.StatusOK, modules)
|
||||
}
|
||||
},
|
||||
Middlewares: []echo.MiddlewareFunc{
|
||||
apis.ActivityLogger(app),
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
app.OnBeforeServe().Add(func(e *core.ServeEvent) error {
|
||||
_, err := e.Router.AddRoute(echo.Route{
|
||||
Method: http.MethodGet,
|
||||
Path: "/api/modules",
|
||||
Handler: func(c echo.Context) error {
|
||||
modules, err := events.GetAllModulesDistinct(app)
|
||||
if err != nil {
|
||||
slog.Error("Failed to get modules: ", "error", err)
|
||||
return c.JSON(http.StatusBadRequest, "Failed to get modules")
|
||||
}
|
||||
return c.JSON(http.StatusOK, modules)
|
||||
},
|
||||
Middlewares: []echo.MiddlewareFunc{
|
||||
apis.ActivityLogger(app),
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
app.OnBeforeServe().Add(func(e *core.ServeEvent) error {
|
||||
_, err := e.Router.AddRoute(echo.Route{
|
||||
Method: http.MethodGet,
|
||||
Path: "/api/module",
|
||||
Handler: func(c echo.Context) error {
|
||||
requestModule := c.QueryParam("uuid")
|
||||
module, err := events.GetModuleByUUID(app, requestModule)
|
||||
if err != nil {
|
||||
slog.Error("Failed to get module: ", "error", err)
|
||||
return c.JSON(http.StatusBadRequest, "Failed to get module")
|
||||
} else {
|
||||
return c.JSON(http.StatusOK, module)
|
||||
}
|
||||
},
|
||||
Middlewares: []echo.MiddlewareFunc{
|
||||
apis.ActivityLogger(app),
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
app.OnBeforeServe().Add(func(e *core.ServeEvent) error {
|
||||
_, err := e.Router.AddRoute(echo.Route{
|
||||
Method: http.MethodGet,
|
||||
Path: "/api/courses",
|
||||
Handler: func(c echo.Context) error {
|
||||
semester := c.QueryParam("semester")
|
||||
if semester == "" {
|
||||
courses := events.GetAllCourses(app)
|
||||
return c.JSON(200, courses)
|
||||
} else {
|
||||
courses := events.GetAllCoursesForSemester(app, semester)
|
||||
return c.JSON(200, courses)
|
||||
}
|
||||
},
|
||||
Middlewares: []echo.MiddlewareFunc{
|
||||
apis.ActivityLogger(app),
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
// api end point to get all courses for a specific semester with courses that have events
|
||||
app.OnBeforeServe().Add(func(e *core.ServeEvent) error {
|
||||
_, err := e.Router.AddRoute(echo.Route{
|
||||
Method: http.MethodGet,
|
||||
Path: "/api/courses/events",
|
||||
Handler: func(c echo.Context) error {
|
||||
semester := c.QueryParam("semester")
|
||||
courses, err := events.GetAllCoursesForSemesterWithEvents(app, semester)
|
||||
|
||||
if err != nil {
|
||||
slog.Error("Failed to get courses for semester with events: ", "error", err)
|
||||
return c.JSON(http.StatusBadRequest, "Failed to get courses for semester with events")
|
||||
} else {
|
||||
return c.JSON(http.StatusOK, courses)
|
||||
}
|
||||
},
|
||||
Middlewares: []echo.MiddlewareFunc{
|
||||
apis.ActivityLogger(app),
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
// API Endpoint to get all eventTypes from the database distinct
|
||||
app.OnBeforeServe().Add(func(e *core.ServeEvent) error {
|
||||
_, err := e.Router.AddRoute(echo.Route{
|
||||
Method: http.MethodGet,
|
||||
Path: "/api/events/types",
|
||||
Handler: func(c echo.Context) error {
|
||||
eventTypes, err := events.GetEventTypes(app)
|
||||
if err != nil {
|
||||
slog.Error("Failed to get event types", "error", err)
|
||||
return c.JSON(http.StatusBadRequest, "Failed to get event types")
|
||||
} else {
|
||||
return c.JSON(http.StatusOK, eventTypes)
|
||||
}
|
||||
},
|
||||
Middlewares: []echo.MiddlewareFunc{
|
||||
apis.ActivityLogger(app),
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
app.OnBeforeServe().Add(func(e *core.ServeEvent) error {
|
||||
_, err := e.Router.AddRoute(echo.Route{
|
||||
Method: http.MethodDelete,
|
||||
Path: "/api/events",
|
||||
Handler: func(c echo.Context) error {
|
||||
err := events.DeleteAllEventsByCourseAndSemester(
|
||||
app,
|
||||
c.QueryParam("course"),
|
||||
c.QueryParam("semester"),
|
||||
)
|
||||
if err != nil {
|
||||
slog.Error("Failed to delete events: ", "error", err)
|
||||
return c.JSON(http.StatusBadRequest, "Failed to delete events")
|
||||
} else {
|
||||
return c.JSON(http.StatusOK, "Events deleted")
|
||||
}
|
||||
},
|
||||
Middlewares: []echo.MiddlewareFunc{
|
||||
apis.ActivityLogger(app),
|
||||
apis.RequireAdminAuth(),
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
86
services/data-manager/service/addSchedule.go
Normal file
86
services/data-manager/service/addSchedule.go
Normal file
@@ -0,0 +1,86 @@
|
||||
//Calendar implementation for the HTWK Leipzig timetable. Evaluation and display of the individual dates in iCal format.
|
||||
//Copyright (C) 2024 HTWKalender support@htwkalender.de
|
||||
|
||||
//This program is free software: you can redistribute it and/or modify
|
||||
//it under the terms of the GNU Affero General Public License as published by
|
||||
//the Free Software Foundation, either version 3 of the License, or
|
||||
//(at your option) any later version.
|
||||
|
||||
//This program is distributed in the hope that it will be useful,
|
||||
//but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
//MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
//GNU Affero General Public License for more details.
|
||||
|
||||
//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/>.
|
||||
|
||||
package service
|
||||
|
||||
import (
|
||||
"github.com/pocketbase/pocketbase"
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
"github.com/pocketbase/pocketbase/tools/cron"
|
||||
"htwkalender/service/course"
|
||||
"htwkalender/service/feed"
|
||||
"htwkalender/service/fetch/sport"
|
||||
v1 "htwkalender/service/fetch/v1"
|
||||
v2 "htwkalender/service/fetch/v2"
|
||||
"htwkalender/service/functions/time"
|
||||
"log/slog"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
func AddSchedules(app *pocketbase.PocketBase) {
|
||||
|
||||
app.OnBeforeServe().Add(func(e *core.ServeEvent) error {
|
||||
scheduler := cron.New()
|
||||
|
||||
// !! IMPORTANT !! CRON is based on UTC time zone so in Germany it is UTC+2 in summer and UTC+1 in winter
|
||||
|
||||
// Every sunday at 10pm update all courses (5 segments - minute, hour, day, month, weekday) "0 22 * * 0"
|
||||
scheduler.MustAdd("updateCourses", "0 22 * * 0", func() {
|
||||
slog.Info("Started updating courses schedule")
|
||||
groups, err := v1.FetchSeminarGroups(app)
|
||||
if err != nil {
|
||||
slog.Warn("Failed to fetch seminar groups: ", "error", err)
|
||||
}
|
||||
slog.Info("Successfully fetched " + strconv.FormatInt(int64(len(groups)), 10) + " seminar groups")
|
||||
})
|
||||
|
||||
// Every day at 5am and 5pm update all courses (5 segments - minute, hour, day, month, weekday) "0 5,17 * * *"
|
||||
// In Germany it is 7am and 7pm, syllabus gets updated twice a day at German 5:00 Uhr and 17:00 Uhr
|
||||
scheduler.MustAdd("updateEventsByCourse", "0 5,17 * * *", func() {
|
||||
slog.Info("Started updating courses schedule")
|
||||
course.UpdateCourse(app)
|
||||
})
|
||||
|
||||
// Every sunday at 1am clean all courses (5 segments - minute, hour, day, month, weekday) "0 3 * * 0"
|
||||
scheduler.MustAdd("cleanFeeds", "0 1 * * 0", func() {
|
||||
// clean feeds older than 6 months
|
||||
slog.Info("Started cleaning feeds schedule")
|
||||
feed.ClearFeeds(app.Dao(), 6, time.RealClock{})
|
||||
})
|
||||
|
||||
// Every sunday at 3am fetch all sport events (5 segments - minute, hour, day, month, weekday) "0 2 * * 0"
|
||||
scheduler.MustAdd("fetchSportEvents", "0 3 * * 0", func() {
|
||||
slog.Info("Started fetching sport events schedule")
|
||||
sportEvents, err := sport.FetchAndUpdateSportEvents(app)
|
||||
if err != nil {
|
||||
slog.Error("Failed to fetch and save sport events:", "error", err)
|
||||
}
|
||||
slog.Info("Successfully fetched " + strconv.FormatInt(int64(len(sportEvents)), 10) + " sport events")
|
||||
})
|
||||
|
||||
//fetch all events for semester and delete from remote this should be done every sunday at 2am
|
||||
scheduler.MustAdd("fetchEvents", "0 22 * * 6", func() {
|
||||
savedEvents, err := v2.FetchAllEventsAndSave(app, time.RealClock{})
|
||||
if err != nil {
|
||||
slog.Error("Failed to fetch and save events: ", "error", err)
|
||||
} else {
|
||||
slog.Info("Successfully fetched " + strconv.FormatInt(int64(len(savedEvents)), 10) + " events")
|
||||
}
|
||||
})
|
||||
scheduler.Start()
|
||||
return nil
|
||||
})
|
||||
}
|
33
services/data-manager/service/course/courseFunctions.go
Normal file
33
services/data-manager/service/course/courseFunctions.go
Normal file
@@ -0,0 +1,33 @@
|
||||
//Calendar implementation for the HTWK Leipzig timetable. Evaluation and display of the individual dates in iCal format.
|
||||
//Copyright (C) 2024 HTWKalender support@htwkalender.de
|
||||
|
||||
//This program is free software: you can redistribute it and/or modify
|
||||
//it under the terms of the GNU Affero General Public License as published by
|
||||
//the Free Software Foundation, either version 3 of the License, or
|
||||
//(at your option) any later version.
|
||||
|
||||
//This program is distributed in the hope that it will be useful,
|
||||
//but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
//MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
//GNU Affero General Public License for more details.
|
||||
|
||||
//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/>.
|
||||
|
||||
package course
|
||||
|
||||
import (
|
||||
"github.com/pocketbase/pocketbase"
|
||||
"htwkalender/service/events"
|
||||
"log/slog"
|
||||
)
|
||||
|
||||
func UpdateCourse(app *pocketbase.PocketBase) {
|
||||
courses := events.GetAllCourses(app)
|
||||
for _, course := range courses {
|
||||
_, err := events.UpdateModulesForCourse(app, course)
|
||||
if err != nil {
|
||||
slog.Warn("Update Course: "+course+" failed:", "error", err)
|
||||
}
|
||||
}
|
||||
}
|
86
services/data-manager/service/date/dateFormat.go
Normal file
86
services/data-manager/service/date/dateFormat.go
Normal file
@@ -0,0 +1,86 @@
|
||||
//Calendar implementation for the HTWK Leipzig timetable. Evaluation and display of the individual dates in iCal format.
|
||||
//Copyright (C) 2024 HTWKalender support@htwkalender.de
|
||||
|
||||
//This program is free software: you can redistribute it and/or modify
|
||||
//it under the terms of the GNU Affero General Public License as published by
|
||||
//the Free Software Foundation, either version 3 of the License, or
|
||||
//(at your option) any later version.
|
||||
|
||||
//This program is distributed in the hope that it will be useful,
|
||||
//but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
//MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
//GNU Affero General Public License for more details.
|
||||
|
||||
//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/>.
|
||||
|
||||
package date
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
_ "time/tzdata"
|
||||
)
|
||||
|
||||
func GetDateFromWeekNumber(year int, weekNumber int, dayName string) (time.Time, error) {
|
||||
// Create a time.Date for the first day of the year
|
||||
europeTime, err := time.LoadLocation("Europe/Berlin")
|
||||
|
||||
if err != nil {
|
||||
slog.Error("Failed to load location: ", "error", err)
|
||||
return time.Time{}, err
|
||||
}
|
||||
|
||||
firstDayOfYear := time.Date(year, time.January, 1, 0, 0, 0, 0, europeTime)
|
||||
|
||||
// Calculate the number of days to add to reach the desired week
|
||||
daysToAdd := time.Duration((weekNumber-1)*7) * 24 * time.Hour
|
||||
|
||||
// Find the starting day of the week (e.g., Monday)
|
||||
startingDayOfWeek := firstDayOfYear
|
||||
|
||||
// check if the first day of the year is friday or saturday or sunday
|
||||
|
||||
if startingDayOfWeek.Weekday() == time.Friday || startingDayOfWeek.Weekday() == time.Saturday || startingDayOfWeek.Weekday() == time.Sunday {
|
||||
for startingDayOfWeek.Weekday() != time.Monday {
|
||||
startingDayOfWeek = startingDayOfWeek.Add(24 * time.Hour)
|
||||
}
|
||||
} else {
|
||||
for startingDayOfWeek.Weekday() != time.Monday {
|
||||
startingDayOfWeek = startingDayOfWeek.Add(-24 * time.Hour)
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate the desired date by adding daysToAdd and adjusting for the day name
|
||||
desiredDate := startingDayOfWeek.Add(daysToAdd)
|
||||
|
||||
// Find the day of the week
|
||||
dayOfWeek := map[string]time.Weekday{
|
||||
"Montag": time.Monday,
|
||||
"Dienstag": time.Tuesday,
|
||||
"Mittwoch": time.Wednesday,
|
||||
"Donnerstag": time.Thursday,
|
||||
"Freitag": time.Friday,
|
||||
"Samstag": time.Saturday,
|
||||
"Sonntag": time.Sunday,
|
||||
}[dayName]
|
||||
|
||||
// Adjust to the desired day of the week
|
||||
for desiredDate.Weekday() != dayOfWeek {
|
||||
desiredDate = desiredDate.Add(24 * time.Hour)
|
||||
}
|
||||
|
||||
return desiredDate, nil
|
||||
}
|
||||
|
||||
// createEventFromTableData should create an event from the table data
|
||||
// tableTime represents Hour and Minute like HH:MM
|
||||
// tableDate returns a Time
|
||||
func CreateTimeFromHourAndMinuteString(tableTime string) time.Time {
|
||||
timeParts := strings.Split(tableTime, ":")
|
||||
hour, _ := strconv.Atoi(timeParts[0])
|
||||
minute, _ := strconv.Atoi(timeParts[1])
|
||||
return time.Date(0, 0, 0, hour, minute, 0, 0, time.UTC)
|
||||
}
|
83
services/data-manager/service/date/dateFormat_test.go
Normal file
83
services/data-manager/service/date/dateFormat_test.go
Normal file
@@ -0,0 +1,83 @@
|
||||
//Calendar implementation for the HTWK Leipzig timetable. Evaluation and display of the individual dates in iCal format.
|
||||
//Copyright (C) 2024 HTWKalender support@htwkalender.de
|
||||
|
||||
//This program is free software: you can redistribute it and/or modify
|
||||
//it under the terms of the GNU Affero General Public License as published by
|
||||
//the Free Software Foundation, either version 3 of the License, or
|
||||
//(at your option) any later version.
|
||||
|
||||
//This program is distributed in the hope that it will be useful,
|
||||
//but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
//MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
//GNU Affero General Public License for more details.
|
||||
|
||||
//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/>.
|
||||
|
||||
package date
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
_ "time/tzdata"
|
||||
)
|
||||
|
||||
func Test_getDateFromWeekNumber(t *testing.T) {
|
||||
europeTime, _ := time.LoadLocation("Europe/Berlin")
|
||||
|
||||
type args struct {
|
||||
year int
|
||||
weekNumber int
|
||||
dayName string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want time.Time
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "Test 1",
|
||||
args: args{
|
||||
year: 2021,
|
||||
weekNumber: 1,
|
||||
dayName: "Montag",
|
||||
},
|
||||
want: time.Date(2021, 1, 4, 0, 0, 0, 0, europeTime),
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "Test 2",
|
||||
args: args{
|
||||
year: 2023,
|
||||
weekNumber: 57,
|
||||
dayName: "Montag",
|
||||
},
|
||||
want: time.Date(2024, 1, 29, 0, 0, 0, 0, europeTime),
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "Test 3",
|
||||
args: args{
|
||||
year: 2023,
|
||||
weekNumber: 1,
|
||||
dayName: "Montag",
|
||||
},
|
||||
want: time.Date(2023, 1, 2, 0, 0, 0, 0, europeTime),
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := GetDateFromWeekNumber(tt.args.year, tt.args.weekNumber, tt.args.dayName)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("getDateFromWeekNumber() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("getDateFromWeekNumber() got = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
400
services/data-manager/service/db/dbEvents.go
Normal file
400
services/data-manager/service/db/dbEvents.go
Normal file
@@ -0,0 +1,400 @@
|
||||
//Calendar implementation for the HTWK Leipzig timetable. Evaluation and display of the individual dates in iCal format.
|
||||
//Copyright (C) 2024 HTWKalender support@htwkalender.de
|
||||
|
||||
//This program is free software: you can redistribute it and/or modify
|
||||
//it under the terms of the GNU Affero General Public License as published by
|
||||
//the Free Software Foundation, either version 3 of the License, or
|
||||
//(at your option) any later version.
|
||||
|
||||
//This program is distributed in the hope that it will be useful,
|
||||
//but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
//MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
//GNU Affero General Public License for more details.
|
||||
|
||||
//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/>.
|
||||
|
||||
package db
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/pocketbase/pocketbase/tools/types"
|
||||
"htwkalender/model"
|
||||
"log/slog"
|
||||
"time"
|
||||
|
||||
"github.com/pocketbase/dbx"
|
||||
"github.com/pocketbase/pocketbase"
|
||||
)
|
||||
|
||||
func SaveSeminarGroupEvents(seminarGroup model.SeminarGroup, app *pocketbase.PocketBase) ([]model.Event, error) {
|
||||
var toBeSavedEvents model.Events
|
||||
var savedRecords model.Events
|
||||
|
||||
// check if event is already in database and add to toBeSavedEvents if not
|
||||
for _, event := range seminarGroup.Events {
|
||||
event = event.SetCourse(seminarGroup.Course)
|
||||
existsInDatabase, err := findEventByDayWeekStartEndNameCourse(event, seminarGroup.Course, app)
|
||||
alreadyAddedToSave := toBeSavedEvents.Contains(event)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !existsInDatabase && !alreadyAddedToSave {
|
||||
toBeSavedEvents = append(toBeSavedEvents, event)
|
||||
}
|
||||
}
|
||||
|
||||
// create record for each event that's not already in the database
|
||||
for _, event := range toBeSavedEvents {
|
||||
event.MarkAsNew()
|
||||
// auto mapping for event fields to record fields
|
||||
err := app.Dao().Save(&event)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
savedRecords = append(savedRecords, event)
|
||||
}
|
||||
}
|
||||
|
||||
return savedRecords, nil
|
||||
}
|
||||
|
||||
func SaveEvents(events []model.Event, app *pocketbase.PocketBase) ([]model.Event, error) {
|
||||
var toBeSavedEvents model.Events
|
||||
var savedRecords model.Events
|
||||
|
||||
// check if event is already in database and add to toBeSavedEvents if not
|
||||
for _, event := range events {
|
||||
existsInDatabase, err := findEventByDayWeekStartEndNameCourse(event, event.Course, app)
|
||||
alreadyAddedToSave := toBeSavedEvents.Contains(event)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !existsInDatabase && !alreadyAddedToSave {
|
||||
toBeSavedEvents = append(toBeSavedEvents, event)
|
||||
}
|
||||
}
|
||||
|
||||
// create record for each event that's not already in the database
|
||||
for _, event := range toBeSavedEvents {
|
||||
event.MarkAsNew()
|
||||
// auto mapping for event fields to record fields
|
||||
err := app.Dao().Save(&event)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
savedRecords = append(savedRecords, event)
|
||||
}
|
||||
}
|
||||
return savedRecords, nil
|
||||
}
|
||||
|
||||
// check if event is already in database and return true if it is and false if it's not
|
||||
func findEventByDayWeekStartEndNameCourse(event model.Event, course string, app *pocketbase.PocketBase) (bool, error) {
|
||||
|
||||
var dbEvent model.Event
|
||||
|
||||
err := app.Dao().DB().Select("*").From("events").
|
||||
Where(dbx.NewExp(
|
||||
"Day = {:day} AND "+
|
||||
"Week = {:week} AND "+
|
||||
"Start = {:start} AND "+
|
||||
"End = {:end} AND "+
|
||||
"Name = {:name} AND "+
|
||||
"course = {:course} AND "+
|
||||
"Prof = {:prof} AND "+
|
||||
"Rooms = {:rooms} AND "+
|
||||
"EventType = {:eventType}",
|
||||
dbx.Params{
|
||||
"day": event.Day,
|
||||
"week": event.Week,
|
||||
"start": event.Start,
|
||||
"end": event.End,
|
||||
"name": event.Name,
|
||||
"course": course,
|
||||
"prof": event.Prof,
|
||||
"rooms": event.Rooms,
|
||||
"eventType": event.EventType}),
|
||||
).One(&dbEvent)
|
||||
|
||||
if err != nil {
|
||||
if err.Error() == "sql: no rows in result set" {
|
||||
return false, nil
|
||||
}
|
||||
return false, err
|
||||
} else {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
|
||||
func buildIcalQueryForModules(modules []model.FeedCollection) dbx.Expression {
|
||||
|
||||
// build where conditions for each module
|
||||
|
||||
//first check if modules is empty
|
||||
if len(modules) == 0 {
|
||||
return dbx.HashExp{}
|
||||
}
|
||||
|
||||
//second check if modules has only one element
|
||||
if len(modules) == 1 {
|
||||
return dbx.HashExp{"uuid": modules[0].UUID}
|
||||
}
|
||||
|
||||
//third check if modules has more than one element
|
||||
var wheres []dbx.Expression
|
||||
|
||||
for _, module := range modules {
|
||||
where := dbx.HashExp{"uuid": module.UUID}
|
||||
wheres = append(wheres, where)
|
||||
}
|
||||
|
||||
// Use dbx.And or dbx.Or to combine the where conditions as needed
|
||||
where := dbx.Or(wheres...)
|
||||
|
||||
return where
|
||||
|
||||
}
|
||||
|
||||
// GetPlanForModules returns all events for the given modules with the given course
|
||||
// used for the ical feed
|
||||
func GetPlanForModules(app *pocketbase.PocketBase, modules map[string]model.FeedCollection) (model.Events, error) {
|
||||
|
||||
var events model.Events
|
||||
|
||||
modulesArray := make([]model.FeedCollection, 0, len(modules))
|
||||
for _, value := range modules {
|
||||
modulesArray = append(modulesArray, value)
|
||||
}
|
||||
|
||||
// iterate over modules in 100 batch sizes
|
||||
for i := 0; i < len(modules); i += 100 {
|
||||
var moduleBatch []model.FeedCollection
|
||||
|
||||
if i+100 > len(modules) {
|
||||
moduleBatch = modulesArray[i:]
|
||||
} else {
|
||||
moduleBatch = modulesArray[i : i+100]
|
||||
}
|
||||
|
||||
var selectedModulesQuery = buildIcalQueryForModules(moduleBatch)
|
||||
// get all events from event records in the events collection
|
||||
err := app.Dao().DB().Select("*").From("events").Where(selectedModulesQuery).OrderBy("start").All(&events)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return events, nil
|
||||
}
|
||||
|
||||
func GetAllEventsForCourse(app *pocketbase.PocketBase, course string) (model.Events, error) {
|
||||
var events model.Events
|
||||
|
||||
// get all events from event records in the events collection
|
||||
err := app.Dao().DB().Select("*").From("events").Where(dbx.NewExp("course = {:course}", dbx.Params{"course": course})).All(&events)
|
||||
if err != nil {
|
||||
slog.Error("Error while getting events from database: ", "error", err)
|
||||
return nil, fmt.Errorf("error while getting events from database for course %s", course)
|
||||
}
|
||||
|
||||
return events, nil
|
||||
}
|
||||
|
||||
func GetAllModulesForCourse(app *pocketbase.PocketBase, course string, semester string) (model.Events, error) {
|
||||
var events model.Events
|
||||
|
||||
// get all events from event records in the events collection
|
||||
err := app.Dao().DB().Select("*").From("events").Where(dbx.NewExp("course = {:course} AND semester = {:semester}", dbx.Params{"course": course, "semester": semester})).GroupBy("Name").Distinct(true).All(&events)
|
||||
if err != nil {
|
||||
slog.Error("Error while getting events from database: ", "error", err)
|
||||
return nil, fmt.Errorf("error while getting events from database for course %s and semester %s", course, semester)
|
||||
}
|
||||
|
||||
return events, nil
|
||||
}
|
||||
|
||||
func GetAllModulesDistinctByNameAndCourse(app *pocketbase.PocketBase) ([]model.ModuleDTO, error) {
|
||||
var modules []model.ModuleDTO
|
||||
|
||||
err := app.Dao().DB().Select("Name", "EventType", "Prof", "course", "semester", "uuid").From("events").GroupBy("Name", "Course").Distinct(true).All(&modules)
|
||||
if err != nil {
|
||||
slog.Error("Error while getting events from database: ", "error", err)
|
||||
return nil, fmt.Errorf("error while getting events distinct by name and course from data")
|
||||
}
|
||||
|
||||
return modules, nil
|
||||
}
|
||||
|
||||
func DeleteAllEventsByCourse(app *pocketbase.PocketBase, course string, semester string) error {
|
||||
_, err := app.Dao().DB().Delete("events", dbx.NewExp("course = {:course} AND semester = {:semester}", dbx.Params{"course": course, "semester": semester})).Execute()
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func DeleteAllEventsBySemesterWithoutCourse(app *pocketbase.PocketBase, course string, semester string) error {
|
||||
_, err := app.Dao().DB().Delete("events", dbx.NewExp("course != {:course} AND semester = {:semester}", dbx.Params{"course": course, "semester": semester})).Execute()
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func DeleteAllEvents(app *pocketbase.PocketBase) error {
|
||||
|
||||
_, err := app.Dao().DB().Delete("events", dbx.NewExp("1=1")).Execute()
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func FindModuleByUUID(app *pocketbase.PocketBase, uuid string) (model.Module, error) {
|
||||
var module model.Module
|
||||
|
||||
err := app.Dao().DB().Select("*").From("events").Where(dbx.NewExp("uuid = {:uuid}", dbx.Params{"uuid": uuid})).One(&module)
|
||||
if err != nil {
|
||||
return model.Module{}, err
|
||||
}
|
||||
|
||||
return module, nil
|
||||
}
|
||||
|
||||
func FindAllEventsByModule(app *pocketbase.PocketBase, module model.Module) (model.Events, error) {
|
||||
var events model.Events
|
||||
|
||||
err := app.Dao().DB().Select("*").From("events").Where(dbx.NewExp("Name = {:moduleName} AND course = {:course}", dbx.Params{"moduleName": module.Name, "course": module.Course})).All(&events)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return events, nil
|
||||
}
|
||||
|
||||
func GetAllModulesByNameAndDateRange(app *pocketbase.PocketBase, name string, startDate time.Time, endDate time.Time) (model.Events, error) {
|
||||
var events model.Events
|
||||
|
||||
err := app.Dao().DB().Select("*").From("events").Where(dbx.NewExp("Name = {:name} AND Start >= {:startDate} AND End <= {:endDate}", dbx.Params{"name": name, "startDate": startDate, "endDate": endDate})).All(&events)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return events, nil
|
||||
}
|
||||
|
||||
// GetEventsThatCollideWithTimeRange returns all events that collide with the given time range
|
||||
// we have events with start and end in the database, we want to get all events that collide with the given time range
|
||||
// we have 4 cases:
|
||||
// 1. event starts before the given time range and ends after the given time range
|
||||
// 2. event starts after the given time range and ends before the given time range
|
||||
// 3. event starts before the given time range and ends before the given time range
|
||||
// 4. event starts after the given time range and ends after the given time range
|
||||
func GetEventsThatCollideWithTimeRange(app *pocketbase.PocketBase, from time.Time, to time.Time) (model.Events, error) {
|
||||
var fromTypeTime, _ = types.ParseDateTime(from)
|
||||
var toTypeTime, _ = types.ParseDateTime(to)
|
||||
|
||||
events1, err := GetEventsThatStartBeforeAndEndAfter(app, fromTypeTime, toTypeTime)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
events2, err := GetEventsThatStartAfterAndEndBefore(app, fromTypeTime, toTypeTime)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
events3, err := GetEventsThatStartBeforeAndEndBefore(app, fromTypeTime, toTypeTime)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
events4, err := GetEventsThatStartAfterAndEndAfter(app, fromTypeTime, toTypeTime)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var events model.Events
|
||||
events = append(events, events1...)
|
||||
events = append(events, events2...)
|
||||
events = append(events, events3...)
|
||||
events = append(events, events4...)
|
||||
|
||||
return events, nil
|
||||
}
|
||||
|
||||
func GetEventsThatStartBeforeAndEndAfter(app *pocketbase.PocketBase, from types.DateTime, to types.DateTime) (model.Events, error) {
|
||||
var events model.Events
|
||||
|
||||
err := app.Dao().DB().Select("*").From("events").Where(dbx.NewExp("Start <= {:startDate} AND End >= {:endDate} AND Start <= {:endDate} AND End >= {:startDate}", dbx.Params{"startDate": from, "endDate": to})).Distinct(true).All(&events)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return events, nil
|
||||
}
|
||||
|
||||
func GetEventsThatStartAfterAndEndBefore(app *pocketbase.PocketBase, from types.DateTime, to types.DateTime) (model.Events, error) {
|
||||
var events model.Events
|
||||
|
||||
err := app.Dao().DB().Select("*").From("events").Where(dbx.NewExp("Start >= {:startDate} AND End <= {:endDate} AND Start <= {:endDate} AND End >= {:startDate}", dbx.Params{"startDate": from, "endDate": to})).All(&events)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return events, nil
|
||||
}
|
||||
|
||||
func GetEventsThatStartBeforeAndEndBefore(app *pocketbase.PocketBase, from types.DateTime, to types.DateTime) (model.Events, error) {
|
||||
var events model.Events
|
||||
|
||||
err := app.Dao().DB().Select("*").From("events").Where(dbx.NewExp("Start <= {:startDate} AND End <= {:endDate} AND Start <= {:endDate} AND End >= {:startDate}", dbx.Params{"startDate": from, "endDate": to})).All(&events)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return events, nil
|
||||
}
|
||||
|
||||
func GetAllEventTypes(app *pocketbase.PocketBase) ([]model.EventType, error) {
|
||||
var eventTypes []model.EventType
|
||||
|
||||
err := app.Dao().DB().Select("EventType").From("events").GroupBy("EventType").Distinct(true).All(&eventTypes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return eventTypes, nil
|
||||
}
|
||||
|
||||
func GetEventsThatStartAfterAndEndAfter(app *pocketbase.PocketBase, from types.DateTime, to types.DateTime) (model.Events, error) {
|
||||
var events model.Events
|
||||
err := app.Dao().DB().Select("*").From("events").Where(dbx.NewExp("Start >= {:startDate} AND End >= {:endDate} AND Start <= {:endDate} AND End >= {:startDate}", dbx.Params{"startDate": from, "endDate": to})).All(&events)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return events, nil
|
||||
}
|
||||
|
||||
func DeleteEvents(list model.Events, app *pocketbase.PocketBase) error {
|
||||
for _, event := range list {
|
||||
err := app.Dao().Delete(&event)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
58
services/data-manager/service/db/dbEvents_test.go
Normal file
58
services/data-manager/service/db/dbEvents_test.go
Normal file
@@ -0,0 +1,58 @@
|
||||
//Calendar implementation for the HTWK Leipzig timetable. Evaluation and display of the individual dates in iCal format.
|
||||
//Copyright (C) 2024 HTWKalender support@htwkalender.de
|
||||
|
||||
//This program is free software: you can redistribute it and/or modify
|
||||
//it under the terms of the GNU Affero General Public License as published by
|
||||
//the Free Software Foundation, either version 3 of the License, or
|
||||
//(at your option) any later version.
|
||||
|
||||
//This program is distributed in the hope that it will be useful,
|
||||
//but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
//MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
//GNU Affero General Public License for more details.
|
||||
|
||||
//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/>.
|
||||
|
||||
package db
|
||||
|
||||
import (
|
||||
"github.com/pocketbase/dbx"
|
||||
"htwkalender/model"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func Test_buildIcalQueryForModules(t *testing.T) {
|
||||
type args struct {
|
||||
modules []model.FeedCollection
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want dbx.Expression
|
||||
}{
|
||||
{
|
||||
name: "empty modules",
|
||||
args: args{modules: []model.FeedCollection{}},
|
||||
want: dbx.HashExp{},
|
||||
},
|
||||
{
|
||||
name: "one module",
|
||||
args: args{modules: []model.FeedCollection{{Name: "test", Course: "test", UUID: "test"}}},
|
||||
want: dbx.HashExp{"uuid": "test"},
|
||||
},
|
||||
{
|
||||
name: "two modules",
|
||||
args: args{modules: []model.FeedCollection{{Name: "test", Course: "test", UUID: "test"}, {Name: "test2", Course: "test2", UUID: "test2"}}},
|
||||
want: dbx.Or(dbx.HashExp{"uuid": "test"}, dbx.HashExp{"uuid": "test2"}),
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := buildIcalQueryForModules(tt.args.modules); !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("buildIcalQueryForModules() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
86
services/data-manager/service/db/dbFeeds.go
Normal file
86
services/data-manager/service/db/dbFeeds.go
Normal file
@@ -0,0 +1,86 @@
|
||||
//Calendar implementation for the HTWK Leipzig timetable. Evaluation and display of the individual dates in iCal format.
|
||||
//Copyright (C) 2024 HTWKalender support@htwkalender.de
|
||||
|
||||
//This program is free software: you can redistribute it and/or modify
|
||||
//it under the terms of the GNU Affero General Public License as published by
|
||||
//the Free Software Foundation, either version 3 of the License, or
|
||||
//(at your option) any later version.
|
||||
|
||||
//This program is distributed in the hope that it will be useful,
|
||||
//but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
//MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
//GNU Affero General Public License for more details.
|
||||
|
||||
//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/>.
|
||||
|
||||
package db
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"github.com/pocketbase/dbx"
|
||||
"github.com/pocketbase/pocketbase"
|
||||
"github.com/pocketbase/pocketbase/daos"
|
||||
"github.com/pocketbase/pocketbase/models"
|
||||
"htwkalender/model"
|
||||
"time"
|
||||
)
|
||||
|
||||
func SaveFeed(feed model.Feed, collection *models.Collection, app *pocketbase.PocketBase) (*models.Record, error) {
|
||||
record := models.NewRecord(collection)
|
||||
record.Set("modules", feed.Modules)
|
||||
err := app.Dao().SaveRecord(record)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return record, nil
|
||||
}
|
||||
|
||||
func FindFeedByToken(token string, app *pocketbase.PocketBase) (*model.Feed, error) {
|
||||
|
||||
record, err := app.Dao().FindRecordById("feeds", token)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var feed model.Feed
|
||||
feed.Modules = record.GetString("modules")
|
||||
feed.Retrieved = record.GetDateTime("retrieved")
|
||||
|
||||
//update retrieved time
|
||||
record.Set("retrieved", time.Now())
|
||||
|
||||
err = app.Dao().SaveRecord(record)
|
||||
|
||||
return &feed, err
|
||||
}
|
||||
|
||||
func DeleteFeed(db *daos.Dao, feedId string) error {
|
||||
|
||||
sqlResult, err := db.DB().Delete("feeds", dbx.NewExp("id = {:id}", dbx.Params{"id": feedId})).Execute()
|
||||
var deletedRows int64
|
||||
if sqlResult != nil {
|
||||
deletedRows, _ = sqlResult.RowsAffected()
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
} else {
|
||||
if deletedRows == 0 {
|
||||
return errors.New("No feed with id " + feedId + " found")
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func GetAllFeeds(db *daos.Dao) ([]model.Feed, error) {
|
||||
var feeds []model.Feed
|
||||
err := db.DB().Select("*").From("feeds").All(&feeds)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return feeds, nil
|
||||
}
|
71
services/data-manager/service/db/dbFeeds_test.go
Normal file
71
services/data-manager/service/db/dbFeeds_test.go
Normal file
@@ -0,0 +1,71 @@
|
||||
//Calendar implementation for the HTWK Leipzig timetable. Evaluation and display of the individual dates in iCal format.
|
||||
//Copyright (C) 2024 HTWKalender support@htwkalender.de
|
||||
|
||||
//This program is free software: you can redistribute it and/or modify
|
||||
//it under the terms of the GNU Affero General Public License as published by
|
||||
//the Free Software Foundation, either version 3 of the License, or
|
||||
//(at your option) any later version.
|
||||
|
||||
//This program is distributed in the hope that it will be useful,
|
||||
//but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
//MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
//GNU Affero General Public License for more details.
|
||||
|
||||
//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/>.
|
||||
|
||||
package db
|
||||
|
||||
import (
|
||||
"github.com/pocketbase/pocketbase/daos"
|
||||
"github.com/pocketbase/pocketbase/tests"
|
||||
"testing"
|
||||
)
|
||||
|
||||
const testDataDir = "./mockData"
|
||||
|
||||
func TestDeleteFeed(t *testing.T) {
|
||||
|
||||
setupTestApp := func(t *testing.T) *daos.Dao {
|
||||
testApp, err := tests.NewTestApp(testDataDir)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
dao := daos.New(testApp.Dao().DB())
|
||||
return dao
|
||||
}
|
||||
|
||||
type args struct {
|
||||
db *daos.Dao
|
||||
feedId string
|
||||
}
|
||||
testsCases := []struct {
|
||||
name string
|
||||
args args
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "TestDeleteFeed",
|
||||
args: args{
|
||||
db: setupTestApp(t),
|
||||
feedId: "fkoqti06ohlnsb8",
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "TestDeleteFeedNotExisting",
|
||||
args: args{
|
||||
db: setupTestApp(t),
|
||||
feedId: "test324",
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range testsCases {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if err := DeleteFeed(tt.args.db, tt.args.feedId); (err != nil) != tt.wantErr {
|
||||
t.Errorf("DeleteFeed() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
27
services/data-manager/service/db/dbFunctions.go
Normal file
27
services/data-manager/service/db/dbFunctions.go
Normal file
@@ -0,0 +1,27 @@
|
||||
//Calendar implementation for the HTWK Leipzig timetable. Evaluation and display of the individual dates in iCal format.
|
||||
//Copyright (C) 2024 HTWKalender support@htwkalender.de
|
||||
|
||||
//This program is free software: you can redistribute it and/or modify
|
||||
//it under the terms of the GNU Affero General Public License as published by
|
||||
//the Free Software Foundation, either version 3 of the License, or
|
||||
//(at your option) any later version.
|
||||
|
||||
//This program is distributed in the hope that it will be useful,
|
||||
//but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
//MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
//GNU Affero General Public License for more details.
|
||||
|
||||
//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/>.
|
||||
|
||||
package db
|
||||
|
||||
import (
|
||||
"github.com/pocketbase/pocketbase"
|
||||
"github.com/pocketbase/pocketbase/models"
|
||||
)
|
||||
|
||||
func FindCollection(app *pocketbase.PocketBase, collectionName string) (*models.Collection, error) {
|
||||
collection, dbError := app.Dao().FindCollectionByNameOrId(collectionName)
|
||||
return collection, dbError
|
||||
}
|
144
services/data-manager/service/db/dbGroups.go
Normal file
144
services/data-manager/service/db/dbGroups.go
Normal file
@@ -0,0 +1,144 @@
|
||||
//Calendar implementation for the HTWK Leipzig timetable. Evaluation and display of the individual dates in iCal format.
|
||||
//Copyright (C) 2024 HTWKalender support@htwkalender.de
|
||||
|
||||
//This program is free software: you can redistribute it and/or modify
|
||||
//it under the terms of the GNU Affero General Public License as published by
|
||||
//the Free Software Foundation, either version 3 of the License, or
|
||||
//(at your option) any later version.
|
||||
|
||||
//This program is distributed in the hope that it will be useful,
|
||||
//but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
//MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
//GNU Affero General Public License for more details.
|
||||
|
||||
//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/>.
|
||||
|
||||
package db
|
||||
|
||||
import (
|
||||
"github.com/pocketbase/dbx"
|
||||
"github.com/pocketbase/pocketbase"
|
||||
"github.com/pocketbase/pocketbase/models"
|
||||
"htwkalender/model"
|
||||
"log/slog"
|
||||
)
|
||||
|
||||
func SaveGroups(seminarGroup []model.SeminarGroup, collection *models.Collection, app *pocketbase.PocketBase) ([]*models.Record, error) {
|
||||
var savedRecords []*models.Record
|
||||
var tobeSavedGroups []model.SeminarGroup
|
||||
var insertRecords []*models.Record
|
||||
|
||||
for _, group := range seminarGroup {
|
||||
dbGroup, err := FindGroupByCourseAndSemester(group.Course, group.Semester, app)
|
||||
|
||||
if dbGroup == nil && err.Error() == "sql: no rows in result set" {
|
||||
tobeSavedGroups = append(tobeSavedGroups, group)
|
||||
} else if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// create record for each group that's not already in the database
|
||||
for _, group := range tobeSavedGroups {
|
||||
record := models.NewRecord(collection)
|
||||
record.Set("university", group.University)
|
||||
record.Set("shortcut", group.GroupShortcut)
|
||||
record.Set("groupId", group.GroupId)
|
||||
record.Set("course", group.Course)
|
||||
record.Set("faculty", group.Faculty)
|
||||
record.Set("facultyId", group.FacultyId)
|
||||
record.Set("semester", group.Semester)
|
||||
insertRecords = append(insertRecords, record)
|
||||
}
|
||||
|
||||
// save all records
|
||||
for _, record := range insertRecords {
|
||||
if record != nil {
|
||||
err := app.Dao().SaveRecord(record)
|
||||
if err == nil {
|
||||
savedRecords = append(savedRecords, record)
|
||||
} else {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return savedRecords, nil
|
||||
}
|
||||
|
||||
func FindGroupByCourseAndSemester(course string, semester string, app *pocketbase.PocketBase) (*model.SeminarGroup, error) {
|
||||
var group model.SeminarGroup
|
||||
err := app.Dao().DB().Select("*").From("groups").Where(dbx.NewExp("course = {:course} AND semester = {:semester}", dbx.Params{"course": course, "semester": semester})).One(&group)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &group, nil
|
||||
}
|
||||
|
||||
func GetAllCourses(app *pocketbase.PocketBase) []string {
|
||||
|
||||
var courses []struct {
|
||||
CourseShortcut string `db:"course" json:"course"`
|
||||
}
|
||||
|
||||
// get all rooms from event records in the events collection
|
||||
err := app.Dao().DB().Select("course").From("groups").All(&courses)
|
||||
if err != nil {
|
||||
slog.Error("Error while getting groups from database: ", "error", err)
|
||||
return []string{}
|
||||
}
|
||||
|
||||
var courseArray []string
|
||||
|
||||
for _, course := range courses {
|
||||
courseArray = append(courseArray, course.CourseShortcut)
|
||||
}
|
||||
|
||||
return courseArray
|
||||
}
|
||||
|
||||
func GetAllCoursesForSemester(app *pocketbase.PocketBase, semester string) []string {
|
||||
|
||||
var courses []struct {
|
||||
CourseShortcut string `db:"course" json:"course"`
|
||||
}
|
||||
|
||||
// get all courses for a specific semester
|
||||
err := app.Dao().DB().Select("course").From("groups").Where(dbx.NewExp("semester = {:semester}", dbx.Params{"semester": semester})).All(&courses)
|
||||
if err != nil {
|
||||
slog.Error("Error while getting groups from database: ", "error", err)
|
||||
return []string{}
|
||||
}
|
||||
|
||||
var courseArray []string
|
||||
|
||||
for _, course := range courses {
|
||||
courseArray = append(courseArray, course.CourseShortcut)
|
||||
}
|
||||
|
||||
return courseArray
|
||||
|
||||
}
|
||||
|
||||
func GetAllCoursesForSemesterWithEvents(app *pocketbase.PocketBase, semester string) ([]string, error) {
|
||||
|
||||
var courses []struct {
|
||||
CourseShortcut string `db:"course" json:"course"`
|
||||
}
|
||||
|
||||
// get all courses from events distinct for a specific semester
|
||||
err := app.Dao().DB().Select("course").From("events").Where(dbx.NewExp("semester = {:semester}", dbx.Params{"semester": semester})).Distinct(true).All(&courses)
|
||||
if err != nil {
|
||||
slog.Error("Error while getting groups from database: ", "error", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var courseArray []string
|
||||
|
||||
for _, course := range courses {
|
||||
courseArray = append(courseArray, course.CourseShortcut)
|
||||
}
|
||||
|
||||
return courseArray, nil
|
||||
}
|
120
services/data-manager/service/db/dbRooms.go
Normal file
120
services/data-manager/service/db/dbRooms.go
Normal file
@@ -0,0 +1,120 @@
|
||||
//Calendar implementation for the HTWK Leipzig timetable. Evaluation and display of the individual dates in iCal format.
|
||||
//Copyright (C) 2024 HTWKalender support@htwkalender.de
|
||||
|
||||
//This program is free software: you can redistribute it and/or modify
|
||||
//it under the terms of the GNU Affero General Public License as published by
|
||||
//the Free Software Foundation, either version 3 of the License, or
|
||||
//(at your option) any later version.
|
||||
|
||||
//This program is distributed in the hope that it will be useful,
|
||||
//but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
//MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
//GNU Affero General Public License for more details.
|
||||
|
||||
//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/>.
|
||||
|
||||
package db
|
||||
|
||||
import (
|
||||
"htwkalender/model"
|
||||
"htwkalender/service/functions"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/pocketbase/dbx"
|
||||
"github.com/pocketbase/pocketbase"
|
||||
)
|
||||
|
||||
func GetRooms(app *pocketbase.PocketBase) ([]string, error) {
|
||||
|
||||
var events []struct {
|
||||
Rooms string `db:"Rooms" json:"Rooms"`
|
||||
Course string `db:"course" json:"Course"`
|
||||
}
|
||||
|
||||
// get all rooms from event records in the events collection
|
||||
err := app.Dao().DB().Select("Rooms", "course").From("events").Distinct(true).All(&events)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
roomArray, err := clearAndSeparateRooms([]struct {
|
||||
Rooms string
|
||||
Course string
|
||||
}(events))
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return roomArray, nil
|
||||
}
|
||||
|
||||
func clearAndSeparateRooms(events []struct {
|
||||
Rooms string
|
||||
Course string
|
||||
}) ([]string, error) {
|
||||
var roomArray []string
|
||||
for _, event := range events {
|
||||
|
||||
var room []string
|
||||
|
||||
// sport rooms don't have to be separated
|
||||
if event.Course != "Sport" {
|
||||
//split rooms by comma, tab, newline, carriage return, semicolon, space and non-breaking space
|
||||
room = functions.SeperateRoomString(event.Rooms)
|
||||
} else {
|
||||
room = append(room, event.Rooms)
|
||||
}
|
||||
|
||||
//split functions room by space and add each room to array if it is not already in there
|
||||
for _, r := range room {
|
||||
var text = strings.TrimSpace(r)
|
||||
if !functions.Contains(roomArray, text) && len(text) >= 1 {
|
||||
roomArray = append(roomArray, text)
|
||||
}
|
||||
}
|
||||
}
|
||||
return roomArray, nil
|
||||
}
|
||||
|
||||
func GetRoomScheduleForDay(app *pocketbase.PocketBase, room string, date string) ([]model.Event, error) {
|
||||
var events []model.Event
|
||||
|
||||
// get all events from event records in the events collection
|
||||
err := app.Dao().DB().Select("*").From("events").
|
||||
Where(dbx.Like("Rooms", room).Escape("_", "_")).
|
||||
AndWhere(dbx.Like("Start", date)).
|
||||
GroupBy("Week", "Start", "End", "Rooms").
|
||||
All(&events)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return events, nil
|
||||
}
|
||||
|
||||
func GetRoomSchedule(app *pocketbase.PocketBase, room string, from string, to string) ([]model.Event, error) {
|
||||
var events []model.Event
|
||||
|
||||
fromDate, err := time.Parse("2006-01-02", from)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
toDate, err := time.Parse("2006-01-02", to)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// get all events from event records in the events collection
|
||||
err = app.Dao().DB().Select("*").From("events").
|
||||
Where(dbx.Like("Rooms", room).Escape("_", "_")).
|
||||
AndWhere(dbx.Between("Start", fromDate, toDate)).
|
||||
GroupBy("Week", "Start", "End", "Rooms").
|
||||
All(&events)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return events, nil
|
||||
}
|
BIN
services/data-manager/service/db/mockData/data.db
Normal file
BIN
services/data-manager/service/db/mockData/data.db
Normal file
Binary file not shown.
BIN
services/data-manager/service/db/mockData/logs.db
Normal file
BIN
services/data-manager/service/db/mockData/logs.db
Normal file
Binary file not shown.
54
services/data-manager/service/events/courseService.go
Normal file
54
services/data-manager/service/events/courseService.go
Normal file
@@ -0,0 +1,54 @@
|
||||
//Calendar implementation for the HTWK Leipzig timetable. Evaluation and display of the individual dates in iCal format.
|
||||
//Copyright (C) 2024 HTWKalender support@htwkalender.de
|
||||
|
||||
//This program is free software: you can redistribute it and/or modify
|
||||
//it under the terms of the GNU Affero General Public License as published by
|
||||
//the Free Software Foundation, either version 3 of the License, or
|
||||
//(at your option) any later version.
|
||||
|
||||
//This program is distributed in the hope that it will be useful,
|
||||
//but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
//MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
//GNU Affero General Public License for more details.
|
||||
|
||||
//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/>.
|
||||
|
||||
package events
|
||||
|
||||
import (
|
||||
"github.com/pocketbase/pocketbase"
|
||||
"htwkalender/service/db"
|
||||
"htwkalender/service/functions"
|
||||
)
|
||||
|
||||
func GetAllCourses(app *pocketbase.PocketBase) []string {
|
||||
return db.GetAllCourses(app)
|
||||
}
|
||||
|
||||
func GetAllCoursesForSemester(app *pocketbase.PocketBase, semester string) []string {
|
||||
return db.GetAllCoursesForSemester(app, semester)
|
||||
}
|
||||
|
||||
func GetAllCoursesForSemesterWithEvents(app *pocketbase.PocketBase, semester string) ([]string, error) {
|
||||
courses, err := db.GetAllCoursesForSemesterWithEvents(app, semester)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
// remove empty courses like " " or ""
|
||||
courses = removeEmptyCourses(courses)
|
||||
return courses, nil
|
||||
}
|
||||
}
|
||||
|
||||
// removeEmptyCourses removes empty courses from the list of courses
|
||||
func removeEmptyCourses(courses []string) []string {
|
||||
var filteredCourses []string
|
||||
|
||||
for index, course := range courses {
|
||||
if !functions.OnlyWhitespace(course) || len(course) != 0 {
|
||||
filteredCourses = append(filteredCourses, courses[index])
|
||||
}
|
||||
}
|
||||
return filteredCourses
|
||||
}
|
55
services/data-manager/service/events/courseService_test.go
Normal file
55
services/data-manager/service/events/courseService_test.go
Normal file
@@ -0,0 +1,55 @@
|
||||
//Calendar implementation for the HTWK Leipzig timetable. Evaluation and display of the individual dates in iCal format.
|
||||
//Copyright (C) 2024 HTWKalender support@htwkalender.de
|
||||
|
||||
//This program is free software: you can redistribute it and/or modify
|
||||
//it under the terms of the GNU Affero General Public License as published by
|
||||
//the Free Software Foundation, either version 3 of the License, or
|
||||
//(at your option) any later version.
|
||||
|
||||
//This program is distributed in the hope that it will be useful,
|
||||
//but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
//MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
//GNU Affero General Public License for more details.
|
||||
|
||||
//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/>.
|
||||
|
||||
package events
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func Test_removeEmptyCourses(t *testing.T) {
|
||||
type args struct {
|
||||
courses []string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want []string
|
||||
}{
|
||||
{
|
||||
name: "Test remove empty courses",
|
||||
args: args{
|
||||
courses: []string{"", "test", "test2", ""},
|
||||
},
|
||||
want: []string{"test", "test2"},
|
||||
},
|
||||
{
|
||||
name: "Test remove empty courses",
|
||||
args: args{
|
||||
courses: []string{"", "test", "test2", "", "test3"},
|
||||
},
|
||||
want: []string{"test", "test2", "test3"},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := removeEmptyCourses(tt.args.courses); !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("removeEmptyCourses() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
215
services/data-manager/service/events/eventService.go
Normal file
215
services/data-manager/service/events/eventService.go
Normal file
@@ -0,0 +1,215 @@
|
||||
//Calendar implementation for the HTWK Leipzig timetable. Evaluation and display of the individual dates in iCal format.
|
||||
//Copyright (C) 2024 HTWKalender support@htwkalender.de
|
||||
|
||||
//This program is free software: you can redistribute it and/or modify
|
||||
//it under the terms of the GNU Affero General Public License as published by
|
||||
//the Free Software Foundation, either version 3 of the License, or
|
||||
//(at your option) any later version.
|
||||
|
||||
//This program is distributed in the hope that it will be useful,
|
||||
//but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
//MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
//GNU Affero General Public License for more details.
|
||||
|
||||
//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/>.
|
||||
|
||||
package events
|
||||
|
||||
import (
|
||||
"github.com/pocketbase/pocketbase"
|
||||
"htwkalender/model"
|
||||
"htwkalender/service/db"
|
||||
"htwkalender/service/fetch/v1"
|
||||
"htwkalender/service/functions"
|
||||
"log/slog"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
func GetModulesForCourseDistinct(app *pocketbase.PocketBase, course string, semester string) (model.Events, error) {
|
||||
|
||||
modules, err := db.GetAllModulesForCourse(app, course, semester)
|
||||
|
||||
// Convert the []model.Module to []Named
|
||||
var namedEvents []Named
|
||||
for _, module := range modules {
|
||||
namedEvents = append(namedEvents, &module)
|
||||
}
|
||||
|
||||
replaceEmptyEntry(namedEvents, "Sonderveranstaltungen")
|
||||
return modules, err
|
||||
}
|
||||
|
||||
type Named interface {
|
||||
GetName() string
|
||||
SetName(name string)
|
||||
}
|
||||
|
||||
// replaceEmptyEntry replaces an empty entry in a module with a replacement string
|
||||
// If the module is not empty, nothing happens
|
||||
func replaceEmptyEntry(namedList []Named, replacement string) {
|
||||
for i, namedItem := range namedList {
|
||||
if functions.OnlyWhitespace(namedItem.GetName()) {
|
||||
namedList[i].SetName(replacement)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// GetAllModulesDistinct returns all modules distinct by name and course from the database
|
||||
// That means you get all modules with duplicates if they have different courses
|
||||
func GetAllModulesDistinct(app *pocketbase.PocketBase) ([]model.ModuleDTO, error) {
|
||||
modules, err := db.GetAllModulesDistinctByNameAndCourse(app)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var namedModules []Named
|
||||
for _, module := range modules {
|
||||
namedModules = append(namedModules, &module)
|
||||
}
|
||||
replaceEmptyEntry(namedModules, "Sonderveranstaltungen")
|
||||
return modules, nil
|
||||
}
|
||||
|
||||
func GetModuleByUUID(app *pocketbase.PocketBase, uuid string) (model.Module, error) {
|
||||
module, findModuleErr := db.FindModuleByUUID(app, uuid)
|
||||
if findModuleErr != nil {
|
||||
return model.Module{}, findModuleErr
|
||||
}
|
||||
|
||||
events, findEventsError := db.FindAllEventsByModule(app, module)
|
||||
if findEventsError != nil || len(events) == 0 {
|
||||
return model.Module{}, findEventsError
|
||||
} else {
|
||||
return model.Module{
|
||||
UUID: events[0].UUID,
|
||||
Name: events[0].Name,
|
||||
Events: events,
|
||||
Prof: events[0].Prof,
|
||||
Course: events[0].Course,
|
||||
Semester: events[0].Semester,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
// DeleteAllEventsByCourseAndSemester deletes all events for a course and a semester
|
||||
// If the deletion was successful, nil is returned
|
||||
// If the deletion was not successful, an error is returned
|
||||
func DeleteAllEventsByCourseAndSemester(app *pocketbase.PocketBase, course string, semester string) error {
|
||||
err := db.DeleteAllEventsByCourse(app, course, semester)
|
||||
if err != nil {
|
||||
return err
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func DeleteAllEvents(app *pocketbase.PocketBase) error {
|
||||
err := db.DeleteAllEvents(app)
|
||||
if err != nil {
|
||||
return err
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// UpdateModulesForCourse updates all modules for a course
|
||||
// Does Updates for ws and ss semester sequentially
|
||||
// Update runs through the following steps:
|
||||
// 1. Delete all events for the course and the semester
|
||||
// 2. Fetch all events for the course and the semester
|
||||
// 3. Save all events for the course and the semester
|
||||
// If the update was successful, nil is returned
|
||||
// If the update was not successful, an error is returned
|
||||
func UpdateModulesForCourse(app *pocketbase.PocketBase, course string) (model.Events, error) {
|
||||
|
||||
seminarGroup := v1.GetSeminarGroupEventsFromHTML(course)
|
||||
|
||||
seminarGroup = v1.ClearEmptySeminarGroups(seminarGroup)
|
||||
|
||||
seminarGroup = v1.ReplaceEmptyEventNames(seminarGroup)
|
||||
|
||||
//check if events in the seminarGroups Events are already in the database
|
||||
//if yes, keep the database as it is
|
||||
//if no, delete all events for the course and the semester and save the new events
|
||||
//if there are no events in the database, save the new events
|
||||
|
||||
//get all events for the course and the semester
|
||||
dbEvents, err := db.GetAllEventsForCourse(app, course)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
//if there are no events in the database, save the new events
|
||||
if len(dbEvents) == 0 {
|
||||
events, dbError := db.SaveSeminarGroupEvents(seminarGroup, app)
|
||||
if dbError != nil {
|
||||
return nil, dbError
|
||||
}
|
||||
return events, nil
|
||||
}
|
||||
|
||||
// Create partial update list and delete list for the events
|
||||
var insertList model.Events
|
||||
var deleteList model.Events
|
||||
|
||||
// check which events are not already in the database and need to be inserted/saved
|
||||
for _, event := range seminarGroup.Events {
|
||||
if !ContainsEvent(dbEvents, event) {
|
||||
insertList = append(insertList, event)
|
||||
}
|
||||
}
|
||||
|
||||
// check which events are in the database but not in the seminarGroup and need to be deleted
|
||||
for _, dbEvent := range dbEvents {
|
||||
if !ContainsEvent(seminarGroup.Events, dbEvent) {
|
||||
deleteList = append(deleteList, dbEvent)
|
||||
}
|
||||
}
|
||||
|
||||
// delete all events that are in the deleteList
|
||||
err = db.DeleteEvents(deleteList, app)
|
||||
if err != nil {
|
||||
slog.Error("Failed to delete events:", "error", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// save all events that are in the insertList
|
||||
savedEvents, err := db.SaveEvents(insertList, app)
|
||||
if err != nil {
|
||||
slog.Error("Failed to save events: ", "error", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
slog.Info("Course: " + course + " - Event changes: " + strconv.FormatInt(int64(len(insertList)), 10) + " new events, " + strconv.FormatInt(int64(len(deleteList)), 10) + " deleted events")
|
||||
return savedEvents, nil
|
||||
}
|
||||
|
||||
func ContainsEvent(events model.Events, event model.Event) bool {
|
||||
for _, e := range events {
|
||||
if e.Name == event.Name &&
|
||||
e.Prof == event.Prof &&
|
||||
e.Rooms == event.Rooms &&
|
||||
e.Semester == event.Semester &&
|
||||
e.Start == event.Start &&
|
||||
e.End == event.End &&
|
||||
e.Course == event.Course {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func GetEventTypes(app *pocketbase.PocketBase) ([]string, error) {
|
||||
dbEventTypes, err := db.GetAllEventTypes(app)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Convert the []model.EventType to []string
|
||||
var eventTypes []string
|
||||
for _, eventType := range dbEventTypes {
|
||||
eventTypes = append(eventTypes, eventType.EventType)
|
||||
}
|
||||
|
||||
return eventTypes, nil
|
||||
}
|
58
services/data-manager/service/events/eventService_test.go
Normal file
58
services/data-manager/service/events/eventService_test.go
Normal file
@@ -0,0 +1,58 @@
|
||||
package events
|
||||
|
||||
import (
|
||||
"htwkalender/model"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestContainsEvent(t *testing.T) {
|
||||
type args struct {
|
||||
events model.Events
|
||||
event model.Event
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "contains event",
|
||||
args: args{
|
||||
events: model.Events{
|
||||
{
|
||||
UUID: "934807509832475",
|
||||
Name: "name",
|
||||
},
|
||||
},
|
||||
event: model.Event{
|
||||
UUID: "934807509832475",
|
||||
Name: "name",
|
||||
},
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "contains no event",
|
||||
args: args{
|
||||
events: model.Events{
|
||||
{
|
||||
UUID: "9991929292921912343534",
|
||||
Name: "Name1",
|
||||
},
|
||||
},
|
||||
event: model.Event{
|
||||
UUID: "1111112312312312",
|
||||
Name: "Name2",
|
||||
},
|
||||
},
|
||||
want: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := ContainsEvent(tt.args.events, tt.args.event); got != tt.want {
|
||||
t.Errorf("ContainsEvent() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
126
services/data-manager/service/feed/feedFunctions.go
Normal file
126
services/data-manager/service/feed/feedFunctions.go
Normal file
@@ -0,0 +1,126 @@
|
||||
//Calendar implementation for the HTWK Leipzig timetable. Evaluation and display of the individual dates in iCal format.
|
||||
//Copyright (C) 2024 HTWKalender support@htwkalender.de
|
||||
|
||||
//This program is free software: you can redistribute it and/or modify
|
||||
//it under the terms of the GNU Affero General Public License as published by
|
||||
//the Free Software Foundation, either version 3 of the License, or
|
||||
//(at your option) any later version.
|
||||
|
||||
//This program is distributed in the hope that it will be useful,
|
||||
//but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
//MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
//GNU Affero General Public License for more details.
|
||||
|
||||
//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/>.
|
||||
|
||||
package feed
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"github.com/pocketbase/dbx"
|
||||
"github.com/pocketbase/pocketbase/daos"
|
||||
"htwkalender/model"
|
||||
database "htwkalender/service/db"
|
||||
localTime "htwkalender/service/functions/time"
|
||||
"log/slog"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func ClearFeeds(db *daos.Dao, months int, clock localTime.Clock) {
|
||||
feeds, err := database.GetAllFeeds(db)
|
||||
if err != nil {
|
||||
slog.Error("CleanFeeds: failed to get all feeds", "error", err)
|
||||
return
|
||||
}
|
||||
for _, feed := range feeds {
|
||||
// if retrieved time is older than a half year delete feed
|
||||
now := clock.Now()
|
||||
feedRetrievedTime := feed.Retrieved.Time()
|
||||
timeShift := now.AddDate(0, -months, 0)
|
||||
|
||||
if feedRetrievedTime.Before(timeShift) {
|
||||
// delete feed
|
||||
var sqlResult sql.Result
|
||||
sqlResult, err = db.DB().Delete("feeds", dbx.NewExp("id = {:id}", dbx.Params{"id": feed.GetId()})).Execute()
|
||||
if err != nil {
|
||||
slog.Error("CleanFeeds: delete feed "+feed.GetId()+" failed", "error", err)
|
||||
slog.Error("SQL Result: ", "error", sqlResult)
|
||||
} else {
|
||||
slog.Info("CleanFeeds: delete feed " + feed.GetId() + " successful")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func CombineEventsInFeed(events model.Events) model.Events {
|
||||
// Combine events with the same name, start, end and course
|
||||
// check if there are events
|
||||
if len(events) > 0 {
|
||||
combinedEvents := model.Events{events[0]}
|
||||
|
||||
for i := 1; i < len(events); i++ {
|
||||
// check if the event is already in the combinedEvents
|
||||
alreadyInCombinedEvents := false
|
||||
|
||||
for j := 0; j < len(combinedEvents); j++ {
|
||||
if events[i].Name == combinedEvents[j].Name &&
|
||||
events[i].Start == combinedEvents[j].Start &&
|
||||
events[i].End == combinedEvents[j].End &&
|
||||
events[i].Course == combinedEvents[j].Course {
|
||||
alreadyInCombinedEvents = true
|
||||
combinedEvents[j].Notes = addNotesIfAlreadyRoomsAdded(events, combinedEvents, j, i)
|
||||
combinedEvents[j].Prof = combineProfs(events, i, combinedEvents, j)
|
||||
combinedEvents[j].Rooms = combineRooms(events, i, combinedEvents, j)
|
||||
break
|
||||
}
|
||||
}
|
||||
if !alreadyInCombinedEvents {
|
||||
combinedEvents = append(combinedEvents, events[i])
|
||||
}
|
||||
}
|
||||
return combinedEvents
|
||||
}
|
||||
return model.Events{}
|
||||
}
|
||||
|
||||
func addNotesIfAlreadyRoomsAdded(events model.Events, combinedEvents model.Events, index2 int, index1 int) string {
|
||||
// check if combinedEvents[index2].Rooms string contains comma "," ,
|
||||
if !strings.Contains(combinedEvents[index2].Rooms, ",") {
|
||||
return descriptionString(combinedEvents[index2]) + "\n" + descriptionString(events[index1])
|
||||
} else {
|
||||
return combinedEvents[index2].Notes + "\n" + descriptionString(events[index1])
|
||||
}
|
||||
}
|
||||
|
||||
func combineProfs(events model.Events, index1 int, combinedEvents model.Events, index2 int) string {
|
||||
// combine the profs
|
||||
if events[index1].Prof != "" {
|
||||
if combinedEvents[index2].Prof == "" {
|
||||
return events[index1].Prof
|
||||
} else {
|
||||
if !strings.Contains(combinedEvents[index2].Prof, events[index1].Prof) {
|
||||
return combinedEvents[index2].Prof + ", " + events[index1].Prof
|
||||
}
|
||||
}
|
||||
}
|
||||
return combinedEvents[index2].Prof
|
||||
}
|
||||
|
||||
func descriptionString(event model.Event) string {
|
||||
return event.Rooms + " - " + event.Notes + " (" + event.Prof + ")"
|
||||
}
|
||||
|
||||
func combineRooms(events model.Events, index1 int, combinedEvents model.Events, index2 int) string {
|
||||
// combine the rooms
|
||||
if events[index1].Rooms != "" {
|
||||
if combinedEvents[index2].Rooms == "" {
|
||||
return events[index1].Rooms
|
||||
} else {
|
||||
if !strings.Contains(combinedEvents[index2].Rooms, events[index1].Rooms) {
|
||||
return combinedEvents[index2].Rooms + ", " + events[index1].Rooms
|
||||
}
|
||||
}
|
||||
}
|
||||
return combinedEvents[index2].Rooms
|
||||
}
|
200
services/data-manager/service/feed/feedFunctions_test.go
Normal file
200
services/data-manager/service/feed/feedFunctions_test.go
Normal file
@@ -0,0 +1,200 @@
|
||||
//Calendar implementation for the HTWK Leipzig timetable. Evaluation and display of the individual dates in iCal format.
|
||||
//Copyright (C) 2024 HTWKalender support@htwkalender.de
|
||||
|
||||
//This program is free software: you can redistribute it and/or modify
|
||||
//it under the terms of the GNU Affero General Public License as published by
|
||||
//the Free Software Foundation, either version 3 of the License, or
|
||||
//(at your option) any later version.
|
||||
|
||||
//This program is distributed in the hope that it will be useful,
|
||||
//but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
//MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
//GNU Affero General Public License for more details.
|
||||
|
||||
//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/>.
|
||||
|
||||
package feed
|
||||
|
||||
import (
|
||||
"github.com/pocketbase/pocketbase/daos"
|
||||
"github.com/pocketbase/pocketbase/tests"
|
||||
"htwkalender/model"
|
||||
mockTime "htwkalender/service/functions/time"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
const testDataDir = "./mockData"
|
||||
|
||||
func TestClearFeeds(t *testing.T) {
|
||||
|
||||
setupTestApp := func(t *testing.T) *daos.Dao {
|
||||
testApp, err := tests.NewTestApp(testDataDir)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
dao := daos.New(testApp.Dao().DB())
|
||||
return dao
|
||||
}
|
||||
|
||||
type args struct {
|
||||
db *daos.Dao
|
||||
months int
|
||||
mockClock mockTime.MockClock
|
||||
}
|
||||
testCases := []struct {
|
||||
name string
|
||||
args args
|
||||
want int
|
||||
}{
|
||||
{
|
||||
name: "TestClearFeeds",
|
||||
args: args{
|
||||
db: setupTestApp(t),
|
||||
months: 6,
|
||||
mockClock: mockTime.MockClock{
|
||||
NowTime: time.Date(2023, 12, 1, 0, 0, 0, 0, time.UTC),
|
||||
},
|
||||
},
|
||||
want: 1,
|
||||
},
|
||||
{
|
||||
name: "TestClearAllFeeds",
|
||||
args: args{
|
||||
db: setupTestApp(t),
|
||||
months: 1,
|
||||
mockClock: mockTime.MockClock{
|
||||
NowTime: time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC),
|
||||
},
|
||||
},
|
||||
want: 0,
|
||||
},
|
||||
{
|
||||
name: "TestClearFeedsClearBeforeRetrievedTime",
|
||||
args: args{
|
||||
db: setupTestApp(t),
|
||||
months: 1,
|
||||
mockClock: mockTime.MockClock{
|
||||
NowTime: time.Date(2010, 1, 1, 0, 0, 0, 0, time.UTC),
|
||||
},
|
||||
},
|
||||
want: 3,
|
||||
},
|
||||
}
|
||||
for _, tt := range testCases {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
ClearFeeds(tt.args.db, tt.args.months, tt.args.mockClock)
|
||||
// count all feeds in db
|
||||
var feeds []*model.Feed
|
||||
err := tt.args.db.DB().Select("id").From("feeds").All(&feeds)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got := len(feeds); got != tt.want {
|
||||
t.Errorf("ClearFeeds() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCombineEventsInFeed(t *testing.T) {
|
||||
type args struct {
|
||||
events model.Events
|
||||
}
|
||||
testCases := []struct {
|
||||
name string
|
||||
args args
|
||||
want model.Events
|
||||
}{
|
||||
{
|
||||
name: "TestCombineEventsInFeed",
|
||||
args: args{
|
||||
events: model.Events{
|
||||
{
|
||||
Name: "Modellierung",
|
||||
Start: mockTime.ParseAsTypesDatetime(time.Date(2023, 12, 1, 0, 0, 0, 0, time.UTC)),
|
||||
End: mockTime.ParseAsTypesDatetime(time.Date(2023, 12, 1, 4, 0, 0, 0, time.UTC)),
|
||||
Prof: "Prof. Bunt",
|
||||
Rooms: "LI001",
|
||||
Notes: "Gruppe 2",
|
||||
},
|
||||
{
|
||||
Name: "Modellierung",
|
||||
Start: mockTime.ParseAsTypesDatetime(time.Date(2023, 12, 1, 0, 0, 0, 0, time.UTC)),
|
||||
End: mockTime.ParseAsTypesDatetime(time.Date(2023, 12, 1, 4, 0, 0, 0, time.UTC)),
|
||||
Prof: "Prof. Bunt",
|
||||
Rooms: "LI002",
|
||||
Notes: "Gruppe 1",
|
||||
},
|
||||
},
|
||||
},
|
||||
want: model.Events{
|
||||
{
|
||||
Name: "Modellierung",
|
||||
Start: mockTime.ParseAsTypesDatetime(time.Date(2023, 12, 1, 0, 0, 0, 0, time.UTC)),
|
||||
End: mockTime.ParseAsTypesDatetime(time.Date(2023, 12, 1, 4, 0, 0, 0, time.UTC)),
|
||||
Prof: "Prof. Bunt",
|
||||
Rooms: "LI001, LI002",
|
||||
Notes: "LI001 - Gruppe 2 (Prof. Bunt)\nLI002 - Gruppe 1 (Prof. Bunt)",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "CannotCombineEventsInFeed",
|
||||
args: args{
|
||||
events: model.Events{
|
||||
{
|
||||
Name: "Modellierung",
|
||||
Start: mockTime.ParseAsTypesDatetime(time.Date(2023, 12, 1, 0, 0, 0, 0, time.UTC)),
|
||||
End: mockTime.ParseAsTypesDatetime(time.Date(2023, 12, 1, 4, 0, 0, 0, time.UTC)),
|
||||
Prof: "Prof. Bunt",
|
||||
Rooms: "LI001",
|
||||
Notes: "Gruppe 2",
|
||||
},
|
||||
{
|
||||
Name: "Modellierung - 2",
|
||||
Start: mockTime.ParseAsTypesDatetime(time.Date(2023, 12, 1, 0, 0, 0, 0, time.UTC)),
|
||||
End: mockTime.ParseAsTypesDatetime(time.Date(2023, 12, 1, 4, 0, 0, 0, time.UTC)),
|
||||
Prof: "Prof. Bunt",
|
||||
Rooms: "LI002",
|
||||
Notes: "Gruppe 1",
|
||||
},
|
||||
},
|
||||
},
|
||||
want: model.Events{
|
||||
{
|
||||
Name: "Modellierung",
|
||||
Start: mockTime.ParseAsTypesDatetime(time.Date(2023, 12, 1, 0, 0, 0, 0, time.UTC)),
|
||||
End: mockTime.ParseAsTypesDatetime(time.Date(2023, 12, 1, 4, 0, 0, 0, time.UTC)),
|
||||
Prof: "Prof. Bunt",
|
||||
Rooms: "LI001",
|
||||
Notes: "Gruppe 2",
|
||||
},
|
||||
{
|
||||
Name: "Modellierung - 2",
|
||||
Start: mockTime.ParseAsTypesDatetime(time.Date(2023, 12, 1, 0, 0, 0, 0, time.UTC)),
|
||||
End: mockTime.ParseAsTypesDatetime(time.Date(2023, 12, 1, 4, 0, 0, 0, time.UTC)),
|
||||
Prof: "Prof. Bunt",
|
||||
Rooms: "LI002",
|
||||
Notes: "Gruppe 1",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "NoEventsInFeed",
|
||||
args: args{
|
||||
events: model.Events{},
|
||||
},
|
||||
want: model.Events{},
|
||||
},
|
||||
}
|
||||
for _, tt := range testCases {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := CombineEventsInFeed(tt.args.events); !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("CombineEventsInFeed() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
BIN
services/data-manager/service/feed/mockData/data.db
Normal file
BIN
services/data-manager/service/feed/mockData/data.db
Normal file
Binary file not shown.
BIN
services/data-manager/service/feed/mockData/logs.db
Normal file
BIN
services/data-manager/service/feed/mockData/logs.db
Normal file
Binary file not shown.
66
services/data-manager/service/fetch/htmlDownloader.go
Normal file
66
services/data-manager/service/fetch/htmlDownloader.go
Normal file
@@ -0,0 +1,66 @@
|
||||
//Calendar implementation for the HTWK Leipzig timetable. Evaluation and display of the individual dates in iCal format.
|
||||
//Copyright (C) 2024 HTWKalender support@htwkalender.de
|
||||
|
||||
//This program is free software: you can redistribute it and/or modify
|
||||
//it under the terms of the GNU Affero General Public License as published by
|
||||
//the Free Software Foundation, either version 3 of the License, or
|
||||
//(at your option) any later version.
|
||||
|
||||
//This program is distributed in the hope that it will be useful,
|
||||
//but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
//MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
//GNU Affero General Public License for more details.
|
||||
|
||||
//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/>.
|
||||
|
||||
package fetch
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
// getPlanHTML Get the HTML document from the specified URL
|
||||
|
||||
func GetHTML(url string) (string, error) {
|
||||
|
||||
// Create HTTP client with timeout of 5 seconds
|
||||
client := http.Client{
|
||||
Timeout: 30 * time.Second,
|
||||
}
|
||||
|
||||
// Send GET request
|
||||
response, err := client.Get(url)
|
||||
if err != nil {
|
||||
fmt.Printf("Error occurred while making the request: %s\n", err.Error())
|
||||
return "", err
|
||||
}
|
||||
defer func(Body io.ReadCloser) {
|
||||
err := Body.Close()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}(response.Body)
|
||||
|
||||
// Read the response body
|
||||
body, err := io.ReadAll(response.Body)
|
||||
|
||||
if err != nil {
|
||||
fmt.Printf("Error occurred while reading the response: %s\n", err.Error())
|
||||
return "", err
|
||||
}
|
||||
|
||||
return toUtf8(body), err
|
||||
|
||||
}
|
||||
|
||||
func toUtf8(iso88591Buf []byte) string {
|
||||
buf := make([]rune, len(iso88591Buf))
|
||||
for i, b := range iso88591Buf {
|
||||
buf[i] = rune(b)
|
||||
}
|
||||
return string(buf)
|
||||
}
|
572
services/data-manager/service/fetch/sport/sportFetcher.go
Normal file
572
services/data-manager/service/fetch/sport/sportFetcher.go
Normal file
@@ -0,0 +1,572 @@
|
||||
//Calendar implementation for the HTWK Leipzig timetable. Evaluation and display of the individual dates in iCal format.
|
||||
//Copyright (C) 2024 HTWKalender support@htwkalender.de
|
||||
|
||||
//This program is free software: you can redistribute it and/or modify
|
||||
//it under the terms of the GNU Affero General Public License as published by
|
||||
//the Free Software Foundation, either version 3 of the License, or
|
||||
//(at your option) any later version.
|
||||
|
||||
//This program is distributed in the hope that it will be useful,
|
||||
//but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
//MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
//GNU Affero General Public License for more details.
|
||||
|
||||
//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/>.
|
||||
|
||||
package sport
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"github.com/google/uuid"
|
||||
"github.com/pocketbase/pocketbase"
|
||||
"github.com/pocketbase/pocketbase/tools/types"
|
||||
"htwkalender/model"
|
||||
"htwkalender/service/db"
|
||||
"htwkalender/service/functions"
|
||||
clock "htwkalender/service/functions/time"
|
||||
"io"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/PuerkitoBio/goquery"
|
||||
)
|
||||
|
||||
// FetchAndUpdateSportEvents fetches all sport events from the HTWK sport website
|
||||
// it deletes them first and then saves them to the database
|
||||
// It returns all saved events
|
||||
func FetchAndUpdateSportEvents(app *pocketbase.PocketBase) ([]model.Event, error) {
|
||||
|
||||
sportCourseLinks, err := fetchAllAvailableSportCourses()
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sportEntries := fetchHTWKSportCourses(sportCourseLinks)
|
||||
events := formatEntriesToEvents(sportEntries)
|
||||
|
||||
var earliestDate time.Time
|
||||
var latestDate time.Time
|
||||
|
||||
// find earliest and latest date in events
|
||||
for _, event := range events {
|
||||
if event.Start.Time().Before(earliestDate) {
|
||||
earliestDate = event.Start.Time()
|
||||
}
|
||||
if event.End.Time().After(latestDate) {
|
||||
latestDate = event.End.Time()
|
||||
}
|
||||
}
|
||||
|
||||
// get all events from database where name = Feiertage und lehrveranstaltungsfreie Tage
|
||||
holidays, err := db.GetAllModulesByNameAndDateRange(app, "Feiertage und lehrveranstaltungsfreie Tage", earliestDate, latestDate)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// remove all events that have same year, month and day as items in holidays
|
||||
for _, holiday := range holidays {
|
||||
for i, event := range events {
|
||||
if event.Start.Time().Year() == holiday.Start.Time().Year() &&
|
||||
event.Start.Time().Month() == holiday.Start.Time().Month() &&
|
||||
event.Start.Time().Day() == holiday.Start.Time().Day() {
|
||||
events = append(events[:i], events[i+1:]...)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// @TODO: delete and save events in one transaction and it only should delete events that are not in the new events list and save events that are not in the database
|
||||
err = db.DeleteAllEventsByCourse(app, "Sport", functions.GetCurrentSemesterString(clock.RealClock{}))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// save events to database
|
||||
savedEvents, err := db.SaveEvents(events, app)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return savedEvents, nil
|
||||
|
||||
}
|
||||
|
||||
func formatEntriesToEvents(entries []model.SportEntry) []model.Event {
|
||||
|
||||
var events []model.Event
|
||||
|
||||
for _, entry := range entries {
|
||||
eventStarts, eventEnds := getWeekEvents(entry.Details.DateRange.Start, entry.Details.DateRange.End, entry.Details.Cycle)
|
||||
for j := range eventStarts {
|
||||
|
||||
start, _ := types.ParseDateTime(eventStarts[j].In(time.UTC))
|
||||
end, _ := types.ParseDateTime(eventEnds[j].In(time.UTC))
|
||||
|
||||
var event = model.Event{
|
||||
UUID: uuid.NewSHA1(uuid.NameSpaceDNS, []byte(entry.Title+entry.ID+entry.Details.Type)).String(),
|
||||
Day: toGermanWeekdayString(start.Time().Weekday()),
|
||||
Week: strconv.Itoa(23),
|
||||
Start: start,
|
||||
End: end,
|
||||
Name: entry.Title + " (" + entry.ID + ")",
|
||||
EventType: entry.Details.Type,
|
||||
Prof: entry.Details.CourseLead.Name,
|
||||
Rooms: entry.Details.Location.Name,
|
||||
Notes: entry.AdditionalNote,
|
||||
BookedAt: "",
|
||||
Course: "Sport",
|
||||
Semester: checkSemester(entry.Details.DateRange.Start),
|
||||
}
|
||||
events = append(events, event)
|
||||
}
|
||||
}
|
||||
return events
|
||||
}
|
||||
|
||||
func getDayInt(weekDay string) (int, error) {
|
||||
var weekDayInt int
|
||||
var err error = nil
|
||||
switch weekDay {
|
||||
case "Mo":
|
||||
weekDayInt = 1
|
||||
case "Di":
|
||||
weekDayInt = 2
|
||||
case "Mi":
|
||||
weekDayInt = 3
|
||||
case "Do":
|
||||
weekDayInt = 4
|
||||
case "Fr":
|
||||
weekDayInt = 5
|
||||
case "Sa":
|
||||
weekDayInt = 6
|
||||
case "So":
|
||||
weekDayInt = 0
|
||||
default:
|
||||
{
|
||||
err = errors.New("no day found")
|
||||
weekDayInt = -1
|
||||
}
|
||||
}
|
||||
return weekDayInt, err
|
||||
}
|
||||
|
||||
func toGermanWeekdayString(weekday time.Weekday) string {
|
||||
switch weekday {
|
||||
case time.Monday:
|
||||
return "Montag"
|
||||
case time.Tuesday:
|
||||
return "Dienstag"
|
||||
case time.Wednesday:
|
||||
return "Mittwoch"
|
||||
case time.Thursday:
|
||||
return "Donnerstag"
|
||||
case time.Friday:
|
||||
return "Freitag"
|
||||
case time.Saturday:
|
||||
return "Samstag"
|
||||
case time.Sunday:
|
||||
return "Sonntag"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func extractStartAndEndTime(cycle string) (int, int, int, int) {
|
||||
timeRegExp, _ := regexp.Compile("[0-9]{2}:[0-9]{2}")
|
||||
times := timeRegExp.FindAllString(cycle, 2)
|
||||
startHour, _ := strconv.Atoi(times[0][0:2])
|
||||
startMinute, _ := strconv.Atoi(times[0][3:5])
|
||||
|
||||
endHour, _ := strconv.Atoi(times[1][0:2])
|
||||
endMinute, _ := strconv.Atoi(times[1][3:5])
|
||||
return startHour, startMinute, endHour, endMinute
|
||||
}
|
||||
|
||||
func getWeekEvents(start time.Time, end time.Time, cycle string) ([]time.Time, []time.Time) {
|
||||
var weekEvents []model.SportDayStartEnd
|
||||
// split by regexp to get the cycle parts
|
||||
var cycleParts = splitByCommaWithTime(cycle)
|
||||
for _, cyclePart := range cycleParts {
|
||||
|
||||
//cut string at the first integer/number
|
||||
cyclePartWithDaysOnly := cyclePart[0:strings.IndexFunc(cyclePart, func(r rune) bool { return r >= '0' && r <= '9' })]
|
||||
|
||||
// check if cycle has multiple days by checking if it has a plus sign
|
||||
if strings.Contains(cyclePartWithDaysOnly, "+") {
|
||||
// find all days in cycle part by regexp
|
||||
dayRegExp, _ := regexp.Compile("[A-Z][a-z]")
|
||||
days := dayRegExp.FindAllString(cyclePart, -1)
|
||||
startHour, startMinute, endHour, endMinute := extractStartAndEndTime(cyclePart)
|
||||
|
||||
// creating a SportDayStartEnd for each day in the cycle
|
||||
for _, day := range days {
|
||||
weekDay, err := getDayInt(day)
|
||||
if err != nil {
|
||||
slog.Error("Error while getting day int: "+day+" ", "error", err)
|
||||
} else {
|
||||
weekEvents = append(weekEvents, model.SportDayStartEnd{
|
||||
Start: time.Date(start.Year(), start.Month(), start.Day(), startHour, startMinute, 0, 0, start.Location()),
|
||||
End: time.Date(end.Year(), end.Month(), end.Day(), endHour, endMinute, 0, 0, end.Location()),
|
||||
Day: time.Weekday(weekDay),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// check if cycle has multiple days by checking if it has a minus sign
|
||||
if strings.Contains(cyclePartWithDaysOnly, "-") {
|
||||
// find all days in cycle part by regexp
|
||||
dayRegExp, _ := regexp.Compile("[A-Z][a-z]")
|
||||
days := dayRegExp.FindAllString(cyclePart, 2)
|
||||
startHour, startMinute, endHour, endMinute := extractStartAndEndTime(cyclePart)
|
||||
|
||||
var startI, endI int
|
||||
var endIErr, startIErr error
|
||||
startI, startIErr = getDayInt(days[0])
|
||||
endI, endIErr = getDayInt(days[1])
|
||||
|
||||
if endIErr != nil || startIErr != nil {
|
||||
slog.Error("StartError while getting day int: "+days[0]+" - "+days[1]+" :", "error", startIErr)
|
||||
slog.Error("EndError while getting day int: "+days[0]+" - "+days[1]+" :", "error", endIErr)
|
||||
} else {
|
||||
//create a int array with all days from start to end day
|
||||
var daysBetween []int
|
||||
for i := startI; i <= endI; i++ {
|
||||
daysBetween = append(daysBetween, i)
|
||||
}
|
||||
|
||||
// creating a SportDayStartEnd for each day in the cycle
|
||||
weekEvents = createEventListFromStartToEndMatchingDay23(daysBetween, start, startHour, startMinute, end, endHour, endMinute)
|
||||
}
|
||||
}
|
||||
|
||||
// check if cycle has only one day
|
||||
if !strings.Contains(cyclePartWithDaysOnly, "-") && !strings.Contains(cyclePartWithDaysOnly, "+") {
|
||||
// find all days in cycle part by regexp
|
||||
dayRegExp, _ := regexp.Compile("[A-Z][a-z]")
|
||||
days := dayRegExp.FindAllString(cyclePart, -1)
|
||||
startHour, startMinute, endHour, endMinute := extractStartAndEndTime(cyclePart)
|
||||
|
||||
var dayNumbers []int
|
||||
for _, day := range days {
|
||||
|
||||
dayInt, err := getDayInt(day)
|
||||
if err != nil {
|
||||
slog.Error("Error while getting day int: "+day+" ", "error", err)
|
||||
} else {
|
||||
dayNumbers = append(dayNumbers, dayInt)
|
||||
}
|
||||
}
|
||||
|
||||
// creating a SportDayStartEnd for each day in the cycle
|
||||
weekEvents = append(weekEvents, createEventListFromStartToEndMatchingDay23(dayNumbers, start, startHour, startMinute, end, endHour, endMinute)...)
|
||||
for _, day := range days {
|
||||
|
||||
weekDay, err := getDayInt(day)
|
||||
if err != nil {
|
||||
slog.Error("Error while getting day int: "+day+" ", "error", err)
|
||||
} else {
|
||||
weekEvents = append(weekEvents, model.SportDayStartEnd{
|
||||
Start: time.Date(start.Year(), start.Month(), start.Day(), startHour, startMinute, 0, 0, start.Location()),
|
||||
End: time.Date(end.Year(), end.Month(), end.Day(), endHour, endMinute, 0, 0, end.Location()),
|
||||
Day: time.Weekday(weekDay),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var startDatesList []time.Time
|
||||
var endDatesList []time.Time
|
||||
|
||||
for _, weekEvent := range weekEvents {
|
||||
startDates, endDates := createEventListFromStartToEndMatchingDay(weekEvent)
|
||||
startDatesList = append(startDatesList, startDates...)
|
||||
endDatesList = append(endDatesList, endDates...)
|
||||
}
|
||||
|
||||
return startDatesList, endDatesList
|
||||
}
|
||||
|
||||
// creating a SportDayStartEnd for each day in the cycle
|
||||
func createEventListFromStartToEndMatchingDay23(days []int, start time.Time, startHour int, startMinute int, end time.Time, endHour int, endMinute int) []model.SportDayStartEnd {
|
||||
|
||||
var weekEvents []model.SportDayStartEnd
|
||||
|
||||
for _, day := range days {
|
||||
weekEvents = append(weekEvents, model.SportDayStartEnd{
|
||||
Start: time.Date(start.Year(), start.Month(), start.Day(), startHour, startMinute, 0, 0, start.Location()),
|
||||
End: time.Date(end.Year(), end.Month(), end.Day(), endHour, endMinute, 0, 0, end.Location()),
|
||||
Day: time.Weekday(day),
|
||||
})
|
||||
}
|
||||
return weekEvents
|
||||
}
|
||||
|
||||
func createEventListFromStartToEndMatchingDay(weekEvent model.SportDayStartEnd) ([]time.Time, []time.Time) {
|
||||
var startDates []time.Time
|
||||
var endDates []time.Time
|
||||
for d := weekEvent.Start; d.Before(weekEvent.End); d = d.AddDate(0, 0, 1) {
|
||||
if d.Weekday() == weekEvent.Day {
|
||||
startDates = append(startDates, time.Date(d.Year(), d.Month(), d.Day(), weekEvent.Start.Hour(), weekEvent.Start.Minute(), 0, 0, d.Location()))
|
||||
endDates = append(endDates, time.Date(d.Year(), d.Month(), d.Day(), weekEvent.End.Hour(), weekEvent.End.Minute(), 0, 0, d.Location()))
|
||||
}
|
||||
}
|
||||
return startDates, endDates
|
||||
}
|
||||
|
||||
func splitByCommaWithTime(input string) []string {
|
||||
var result []string
|
||||
|
||||
// Split by comma
|
||||
parts := strings.Split(input, ", ")
|
||||
|
||||
// Regular expression to match a day with time
|
||||
regex := regexp.MustCompile(`([A-Za-z]{2,}(\+[A-Za-z]{2,})* \d{2}:\d{2}-\d{2}:\d{2})`)
|
||||
|
||||
// Iterate over parts and combine when necessary
|
||||
var currentPart string
|
||||
for _, part := range parts {
|
||||
if regex.MatchString(part) {
|
||||
if currentPart != "" {
|
||||
currentPart += ", " + part
|
||||
result = append(result, currentPart)
|
||||
currentPart = ""
|
||||
} else {
|
||||
result = append(result, part)
|
||||
}
|
||||
// If the part contains a day with time, start a new currentPart
|
||||
|
||||
} else {
|
||||
// If there's no currentPart, start a new one
|
||||
if currentPart != "" {
|
||||
currentPart += ", " + part
|
||||
} else {
|
||||
currentPart = part
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add the last currentPart to the result
|
||||
if currentPart != "" {
|
||||
result = append(result, currentPart)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// check if ws or ss
|
||||
func checkSemester(date time.Time) string {
|
||||
if date.Month() >= 4 && date.Month() <= 9 {
|
||||
return "ss"
|
||||
} else {
|
||||
return "ws"
|
||||
}
|
||||
}
|
||||
|
||||
// fetch the main page where all sport courses are listed and extract all links to the sport courses
|
||||
func fetchAllAvailableSportCourses() ([]string, error) {
|
||||
var url = "https://sport.htwk-leipzig.de/sportangebote"
|
||||
|
||||
var doc, err = htmlRequest(url)
|
||||
|
||||
if err != nil {
|
||||
slog.Error("Error while fetching sport courses from webpage", "error", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// link list of all sport courses
|
||||
var links []string
|
||||
|
||||
// find all links to sport courses with regex https://sport.htwk-leipzig.de/sportangebote/detail/sport/ + [0-9]{1,4}
|
||||
doc.Find("a[href]").Each(func(i int, s *goquery.Selection) {
|
||||
link, _ := s.Attr("href")
|
||||
if strings.HasPrefix(link, "/sportangebote/detail/sport/") {
|
||||
links = append(links, link)
|
||||
}
|
||||
})
|
||||
|
||||
return links, nil
|
||||
}
|
||||
|
||||
// fetchAllHTWKSportCourses fetches all sport courses from the given links.
|
||||
// to speed up the process, it uses multithreading.
|
||||
|
||||
func fetchHTWKSportCourses(links []string) []model.SportEntry {
|
||||
|
||||
//multithreaded webpage requests to speed up the process
|
||||
|
||||
var maxThreads = 10
|
||||
var htmlPageArray = make([]*goquery.Document, len(links))
|
||||
var hostUrl = "https://sport.htwk-leipzig.de"
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(maxThreads)
|
||||
for i := 0; i < maxThreads; i++ {
|
||||
go func(i int) {
|
||||
for j := i; j < len(links); j += maxThreads {
|
||||
doc, err := htmlRequest(hostUrl + links[j])
|
||||
if err == nil {
|
||||
htmlPageArray[j] = doc
|
||||
}
|
||||
}
|
||||
wg.Done()
|
||||
}(i)
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
var events []model.SportEntry
|
||||
|
||||
for _, doc := range htmlPageArray {
|
||||
if doc != nil {
|
||||
event, err := fetchHtwkSportCourse(doc)
|
||||
if err == nil {
|
||||
events = append(events, event...)
|
||||
}
|
||||
}
|
||||
}
|
||||
return events
|
||||
}
|
||||
|
||||
func htmlRequest(url string) (*goquery.Document, error) {
|
||||
resp, err := http.Get(url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer func(Body io.ReadCloser) {
|
||||
readErr := Body.Close()
|
||||
if readErr != nil {
|
||||
slog.Error("Error while closing response body from html request", "error", readErr)
|
||||
return
|
||||
}
|
||||
}(resp.Body)
|
||||
|
||||
doc, err := goquery.NewDocumentFromReader(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return doc, nil
|
||||
}
|
||||
|
||||
// fetchHtwkSportCourse fetches the sport course from the given url and id.
|
||||
// If the sport course does not exist, it will return an error.
|
||||
// If the sport course exists, it will return the sport course.
|
||||
// goquery is used to parse the html. The html structure is not very consistent, so it is hard to parse.
|
||||
// May be improved in the future.
|
||||
func fetchHtwkSportCourse(doc *goquery.Document) ([]model.SportEntry, error) {
|
||||
var events []model.SportEntry
|
||||
germanTime, _ := time.LoadLocation("Europe/Berlin")
|
||||
|
||||
if doc.Find("h1").Text() == "Aktuelle Sportangebote" {
|
||||
return nil, errors.New("not a sport course page")
|
||||
}
|
||||
|
||||
doc.Find(".eventHead").Each(func(i int, s *goquery.Selection) {
|
||||
var event model.SportEntry
|
||||
var details model.EventDetails
|
||||
|
||||
fullTitle := strings.TrimSpace(s.Find("h3").Text())
|
||||
titleParts := strings.Split(fullTitle, "-")
|
||||
if len(titleParts) > 0 {
|
||||
event.Title = strings.TrimSpace(titleParts[0])
|
||||
}
|
||||
|
||||
if len(titleParts) > 2 {
|
||||
details.Type = strings.TrimSpace(titleParts[len(titleParts)-1])
|
||||
}
|
||||
|
||||
event.ID = parseEventID(fullTitle)
|
||||
|
||||
s.NextFiltered("table.eventDetails").Find("tr").Each(func(i int, s *goquery.Selection) {
|
||||
key := strings.TrimSpace(s.Find("td").First().Text())
|
||||
value := strings.TrimSpace(s.Find("td").Last().Text())
|
||||
|
||||
switch key {
|
||||
case "Zeitraum":
|
||||
dates := strings.Split(value, "-")
|
||||
if len(dates) == 2 {
|
||||
startDate, _ := time.ParseInLocation("02.01.2006", strings.TrimSpace(dates[0]), germanTime)
|
||||
endDate, _ := time.ParseInLocation("02.01.2006", strings.TrimSpace(dates[1]), germanTime)
|
||||
details.DateRange = model.DateRange{Start: startDate, End: endDate}
|
||||
}
|
||||
case "Zyklus":
|
||||
details.Cycle = value
|
||||
case "Geschlecht":
|
||||
details.Gender = value
|
||||
case "Leiter":
|
||||
leaderName := strings.TrimSpace(s.Find("td a").Text())
|
||||
leadersSlice := strings.Split(leaderName, "\n")
|
||||
for i, leader := range leadersSlice {
|
||||
leadersSlice[i] = strings.TrimSpace(leader)
|
||||
}
|
||||
formattedLeaders := strings.Join(leadersSlice, ", ")
|
||||
leaderLink, _ := s.Find("td a").Attr("href")
|
||||
details.CourseLead = model.CourseLead{Name: formattedLeaders, Link: leaderLink}
|
||||
case "Ort":
|
||||
locationDetails := strings.Split(value, "(")
|
||||
if len(locationDetails) == 2 {
|
||||
details.Location = model.Location{
|
||||
Name: strings.TrimSpace(locationDetails[0]),
|
||||
Address: strings.TrimRight(strings.TrimSpace(locationDetails[1]), ")"),
|
||||
}
|
||||
}
|
||||
case "Teilnehmer":
|
||||
parts := strings.Split(value, "/")
|
||||
if len(parts) >= 3 {
|
||||
bookings, _ := strconv.Atoi(strings.TrimSpace(parts[0]))
|
||||
totalPlaces, _ := strconv.Atoi(strings.TrimSpace(parts[1]))
|
||||
waitList, _ := strconv.Atoi(strings.TrimSpace(parts[2]))
|
||||
details.Participants = model.Participants{Bookings: bookings, TotalPlaces: totalPlaces, WaitList: waitList}
|
||||
}
|
||||
case "Kosten":
|
||||
details.Cost = value // makes no sense since you need to be logged in to see the price
|
||||
case "Hinweis":
|
||||
var allNotes []string
|
||||
|
||||
s.Find("td").Last().Contents().Each(func(i int, s *goquery.Selection) {
|
||||
if s.Is("h4.eventAdvice") || goquery.NodeName(s) == "#text" {
|
||||
note := strings.TrimSpace(s.Text())
|
||||
if note != "" {
|
||||
allNotes = append(allNotes, note)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
event.AdditionalNote = strings.Join(allNotes, " ")
|
||||
}
|
||||
})
|
||||
|
||||
event.Details = details
|
||||
events = append(events, event)
|
||||
})
|
||||
|
||||
return events, nil
|
||||
}
|
||||
|
||||
// parseEventID from fulltitle
|
||||
// the event id is a number in the fulltitle thats not a time like HH:MM and shoudl be found after Nr. or Nr:
|
||||
func parseEventID(fulltitle string) string {
|
||||
var eventID string
|
||||
var numberRegExp = regexp.MustCompile("[0-9]{1,4}")
|
||||
var fulltitleParts = strings.Split(fulltitle, " ")
|
||||
for i, part := range fulltitleParts {
|
||||
if part == "Nr." || part == "Nr:" {
|
||||
eventID = fulltitleParts[i+1]
|
||||
break
|
||||
}
|
||||
}
|
||||
if eventID == "" {
|
||||
eventID = numberRegExp.FindString(fulltitle)
|
||||
}
|
||||
return eventID
|
||||
|
||||
}
|
@@ -0,0 +1,56 @@
|
||||
//Calendar implementation for the HTWK Leipzig timetable. Evaluation and display of the individual dates in iCal format.
|
||||
//Copyright (C) 2024 HTWKalender support@htwkalender.de
|
||||
|
||||
//This program is free software: you can redistribute it and/or modify
|
||||
//it under the terms of the GNU Affero General Public License as published by
|
||||
//the Free Software Foundation, either version 3 of the License, or
|
||||
//(at your option) any later version.
|
||||
|
||||
//This program is distributed in the hope that it will be useful,
|
||||
//but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
//MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
//GNU Affero General Public License for more details.
|
||||
|
||||
//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/>.
|
||||
|
||||
package sport
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func Test_splitByCommaWithTime(t *testing.T) {
|
||||
type args struct {
|
||||
input string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want []string
|
||||
}{
|
||||
{"one string", args{"one"}, []string{"one"}},
|
||||
{"two strings", args{"one,two"}, []string{"one,two"}},
|
||||
{"three strings", args{"one,two,three"}, []string{"one,two,three"}},
|
||||
// e.g. "Mo 18:00-20:00, Di 18:00-20:00" -> ["Mo 18:00-20:00", "Di 18:00-20:00"]
|
||||
// e.g. "Mo 18:00-20:00, Di 18:00-20:00, Mi 18:00-20:00" -> ["Mo 18:00-20:00", "Di 18:00-20:00", "Mi 18:00-20:00"]
|
||||
// e.g. "Mo, Mi, Fr 18:00-20:00, Sa 20:00-21:00" -> ["Mo, Mi, Fr 18:00-20:00", "Sa 20:00-21:00"]
|
||||
// e.g. "Mo, Mi, Fr 18:00-20:00, Sa 20:00-21:00, So 20:00-21:00" -> ["Mo, Mi, Fr 18:00-20:00", "Sa 20:00-21:00", "So 20:00-21:00"]
|
||||
// e.g. "Mo+Mi+Fr 18:00-20:00, Sa 20:00-21:00" -> ["Mo+Mi+Fr 18:00-20:00", "Sa 20:00-21:00"]
|
||||
// e.g. "Mo+Mi 18:00-20:00, Sa 20:00-21:00, So 20:00-21:00" -> ["Mo+Mi 18:00-20:00", "Sa 20:00-21:00", "So 20:00-21:00"]
|
||||
{"Mo 18:00-20:00, Di 18:00-20:00", args{"Mo 18:00-20:00, Di 18:00-20:00"}, []string{"Mo 18:00-20:00", "Di 18:00-20:00"}},
|
||||
{"Mo 18:00-20:00, Di 18:00-20:00, Mi 18:00-20:00", args{"Mo 18:00-20:00, Di 18:00-20:00, Mi 18:00-20:00"}, []string{"Mo 18:00-20:00", "Di 18:00-20:00", "Mi 18:00-20:00"}},
|
||||
{"Mo, Mi, Fr 18:00-20:00, Sa 20:00-21:00", args{"Mo, Mi, Fr 18:00-20:00, Sa 20:00-21:00"}, []string{"Mo, Mi, Fr 18:00-20:00", "Sa 20:00-21:00"}},
|
||||
{"Mo, Mi, Fr 18:00-20:00, Sa 20:00-21:00, So 20:00-21:00", args{"Mo, Mi, Fr 18:00-20:00, Sa 20:00-21:00, So 20:00-21:00"}, []string{"Mo, Mi, Fr 18:00-20:00", "Sa 20:00-21:00", "So 20:00-21:00"}},
|
||||
{"Mo+Mi+Fr 18:00-20:00, Sa 20:00-21:00", args{"Mo+Mi+Fr 18:00-20:00, Sa 20:00-21:00"}, []string{"Mo+Mi+Fr 18:00-20:00", "Sa 20:00-21:00"}},
|
||||
{"Mo+Mi 18:00-20:00, Sa 20:00-21:00, So 20:00-21:00", args{"Mo+Mi 18:00-20:00, Sa 20:00-21:00, So 20:00-21:00"}, []string{"Mo+Mi 18:00-20:00", "Sa 20:00-21:00", "So 20:00-21:00"}},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := splitByCommaWithTime(tt.args.input); !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("splitByCommaWithTime() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
@@ -0,0 +1,297 @@
|
||||
//Calendar implementation for the HTWK Leipzig timetable. Evaluation and display of the individual dates in iCal format.
|
||||
//Copyright (C) 2024 HTWKalender support@htwkalender.de
|
||||
|
||||
//This program is free software: you can redistribute it and/or modify
|
||||
//it under the terms of the GNU Affero General Public License as published by
|
||||
//the Free Software Foundation, either version 3 of the License, or
|
||||
//(at your option) any later version.
|
||||
|
||||
//This program is distributed in the hope that it will be useful,
|
||||
//but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
//MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
//GNU Affero General Public License for more details.
|
||||
|
||||
//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/>.
|
||||
|
||||
package v1
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/google/uuid"
|
||||
"github.com/pocketbase/pocketbase/tools/types"
|
||||
"golang.org/x/net/html"
|
||||
"htwkalender/model"
|
||||
"htwkalender/service/date"
|
||||
"htwkalender/service/fetch"
|
||||
"htwkalender/service/functions"
|
||||
"log/slog"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
func ReplaceEmptyEventNames(group model.SeminarGroup) model.SeminarGroup {
|
||||
for j, event := range group.Events {
|
||||
if functions.OnlyWhitespace(event.Name) {
|
||||
group.Events[j].Name = "Sonderveranstaltungen"
|
||||
}
|
||||
}
|
||||
return group
|
||||
}
|
||||
|
||||
func ClearEmptySeminarGroups(seminarGroup model.SeminarGroup) model.SeminarGroup {
|
||||
var newSeminarGroup = model.SeminarGroup{}
|
||||
|
||||
if len(seminarGroup.Events) > 0 && seminarGroup.Course != "" {
|
||||
newSeminarGroup = seminarGroup
|
||||
}
|
||||
return newSeminarGroup
|
||||
}
|
||||
|
||||
func GetSeminarGroupEventsFromHTML(seminarGroupLabel string) model.SeminarGroup {
|
||||
var seminarGroup model.SeminarGroup
|
||||
|
||||
if (time.Now().Month() >= 3) && (time.Now().Month() <= 10) {
|
||||
ssUrl := "https://stundenplan.htwk-leipzig.de/" + string("ss") + "/Berichte/Text-Listen;Studenten-Sets;name;" + seminarGroupLabel + "?template=sws_semgrp&weeks=1-65"
|
||||
result, getError := fetch.GetHTML(ssUrl)
|
||||
if getError == nil {
|
||||
seminarGroup = parseSeminarGroup(result)
|
||||
}
|
||||
}
|
||||
|
||||
if (time.Now().Month() >= 9) || (time.Now().Month() <= 4) {
|
||||
wsUrl := "https://stundenplan.htwk-leipzig.de/" + string("ws") + "/Berichte/Text-Listen;Studenten-Sets;name;" + seminarGroupLabel + "?template=sws_semgrp&weeks=1-65"
|
||||
result, getError := fetch.GetHTML(wsUrl)
|
||||
if getError == nil {
|
||||
seminarGroup = parseSeminarGroup(result)
|
||||
}
|
||||
}
|
||||
return seminarGroup
|
||||
}
|
||||
|
||||
func SplitEventType(events []model.Event) ([]model.Event, error) {
|
||||
re, err := regexp.Compile("^([VPS])([wp])$")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for i, event := range events {
|
||||
matched := re.Match([]byte(event.EventType))
|
||||
if matched {
|
||||
eventType := event.EventType
|
||||
event.EventType = eventType[0:1]
|
||||
event.Compulsory = eventType[1:2]
|
||||
events[i] = event
|
||||
}
|
||||
}
|
||||
return events, nil
|
||||
}
|
||||
|
||||
func parseSeminarGroup(result string) model.SeminarGroup {
|
||||
doc, err := html.Parse(strings.NewReader(result))
|
||||
if err != nil {
|
||||
fmt.Printf("Error occurred while parsing the HTML document: %s\n", err.Error())
|
||||
return model.SeminarGroup{}
|
||||
}
|
||||
|
||||
table := findFirstTable(doc)
|
||||
eventTables := getEventTables(doc)
|
||||
allDayLabels := getAllDayLabels(doc)
|
||||
|
||||
if eventTables == nil || allDayLabels == nil {
|
||||
return model.SeminarGroup{}
|
||||
}
|
||||
course := findFirstSpanWithClass(table, "header-2-0-1").FirstChild.Data
|
||||
eventsWithCombinedWeeks := toEvents(eventTables, allDayLabels, course)
|
||||
splitEventsByWeekVal := splitEventsByWeek(eventsWithCombinedWeeks)
|
||||
events := splitEventsBySingleWeek(splitEventsByWeekVal)
|
||||
semesterString := findFirstSpanWithClass(table, "header-0-2-0").FirstChild.Data
|
||||
semester, year := extractSemesterAndYear(semesterString)
|
||||
events = convertWeeksToDates(events, semester, year)
|
||||
events = generateUUIDs(events, course)
|
||||
events, err = SplitEventType(events)
|
||||
|
||||
if err != nil {
|
||||
slog.Error("Error occurred while splitting event types:", "error", err)
|
||||
return model.SeminarGroup{}
|
||||
}
|
||||
|
||||
var seminarGroup = model.SeminarGroup{
|
||||
University: findFirstSpanWithClass(table, "header-1-0-0").FirstChild.Data,
|
||||
Course: course,
|
||||
Events: events,
|
||||
}
|
||||
return seminarGroup
|
||||
}
|
||||
|
||||
func generateUUIDs(events []model.Event, course string) []model.Event {
|
||||
for i, event := range events {
|
||||
// generate a hash value from the event name, course and semester
|
||||
hash := uuid.NewSHA1(uuid.NameSpaceOID, []byte(event.Name+course))
|
||||
events[i].UUID = hash.String()
|
||||
}
|
||||
return events
|
||||
}
|
||||
|
||||
// convertWeeksToDates converts the week and year to a date
|
||||
// The date is calculated based on the week and the year
|
||||
// The time is unset and 23:00 is used as default
|
||||
// Additionally the semester is added to the event
|
||||
|
||||
func convertWeeksToDates(events []model.Event, semester string, year string) []model.Event {
|
||||
var newEvents []model.Event
|
||||
eventYear, _ := strconv.Atoi(year)
|
||||
|
||||
// for each event we need to calculate the start and end date based on the week and the year
|
||||
for _, event := range events {
|
||||
eventWeek, _ := strconv.Atoi(event.Week)
|
||||
eventDay, _ := date.GetDateFromWeekNumber(eventYear, eventWeek, event.Day)
|
||||
start := replaceTimeForDate(eventDay, event.Start.Time())
|
||||
end := replaceTimeForDate(eventDay, event.End.Time())
|
||||
|
||||
//Check if end is before start
|
||||
if end.Before(start) {
|
||||
end = end.AddDate(0, 0, 1)
|
||||
}
|
||||
|
||||
newEvent := event
|
||||
newEvent.Start, _ = types.ParseDateTime(start.In(time.UTC))
|
||||
newEvent.End, _ = types.ParseDateTime(end.In(time.UTC))
|
||||
newEvent.Semester = semester
|
||||
newEvents = append(newEvents, newEvent)
|
||||
}
|
||||
return newEvents
|
||||
}
|
||||
|
||||
// replaceTimeForDate replaces hour, minute, second, nsec for the selected date
|
||||
func replaceTimeForDate(date time.Time, replacementTime time.Time) time.Time {
|
||||
return time.Date(date.Year(), date.Month(), date.Day(), replacementTime.Hour(), replacementTime.Minute(), replacementTime.Second(), replacementTime.Nanosecond(), date.Location())
|
||||
}
|
||||
|
||||
func extractSemesterAndYear(semesterString string) (string, string) {
|
||||
winterPattern := "Wintersemester"
|
||||
summerPattern := "Sommersemester"
|
||||
|
||||
winterMatch := strings.Contains(semesterString, winterPattern)
|
||||
summerMatch := strings.Contains(semesterString, summerPattern)
|
||||
|
||||
semester := ""
|
||||
semesterShortcut := ""
|
||||
|
||||
if winterMatch {
|
||||
semester = "Wintersemester"
|
||||
semesterShortcut = "ws"
|
||||
} else if summerMatch {
|
||||
semester = "Sommersemester"
|
||||
semesterShortcut = "ss"
|
||||
} else {
|
||||
return "", ""
|
||||
}
|
||||
|
||||
yearPattern := `\d{4}`
|
||||
combinedPattern := semester + `\s` + yearPattern
|
||||
re := regexp.MustCompile(combinedPattern)
|
||||
match := re.FindString(semesterString)
|
||||
year := ""
|
||||
|
||||
if match != "" {
|
||||
reYear := regexp.MustCompile(yearPattern)
|
||||
year = reYear.FindString(match)
|
||||
}
|
||||
return semesterShortcut, year
|
||||
}
|
||||
|
||||
func toEvents(tables [][]*html.Node, days []string, course string) []model.Event {
|
||||
var events []model.Event
|
||||
|
||||
for table := range tables {
|
||||
for row := range tables[table] {
|
||||
|
||||
tableData := findTableData(tables[table][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],
|
||||
Week: getTextContent(tableData[0]),
|
||||
Start: start,
|
||||
End: end,
|
||||
Name: getTextContent(tableData[3]),
|
||||
EventType: getTextContent(tableData[4]),
|
||||
Prof: getTextContent(tableData[5]),
|
||||
Rooms: getTextContent(tableData[6]),
|
||||
Notes: getTextContent(tableData[7]),
|
||||
BookedAt: getTextContent(tableData[8]),
|
||||
Course: course,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
return events
|
||||
}
|
||||
|
||||
// createEventFromTableData should create an event from the table data
|
||||
// tableTime represents Hour and Minute like HH:MM
|
||||
// tableDate returns a Time
|
||||
func createTimeFromHourAndMinuteString(tableTime string) time.Time {
|
||||
timeParts := strings.Split(tableTime, ":")
|
||||
hour, _ := strconv.Atoi(timeParts[0])
|
||||
minute, _ := strconv.Atoi(timeParts[1])
|
||||
return time.Date(0, 0, 0, hour, minute, 0, 0, time.UTC)
|
||||
}
|
||||
|
||||
func splitEventsByWeek(events []model.Event) []model.Event {
|
||||
var newEvents []model.Event
|
||||
|
||||
for _, event := range events {
|
||||
weeks := strings.Split(event.Week, ",")
|
||||
for _, week := range weeks {
|
||||
newEvent := event
|
||||
newEvent.Week = strings.TrimSpace(week)
|
||||
newEvents = append(newEvents, newEvent)
|
||||
}
|
||||
}
|
||||
return newEvents
|
||||
}
|
||||
|
||||
func splitEventsBySingleWeek(events []model.Event) []model.Event {
|
||||
var newEvents []model.Event
|
||||
|
||||
for _, event := range events {
|
||||
if strings.Contains(event.Week, "-") {
|
||||
weeks := splitWeekRange(event.Week)
|
||||
for _, week := range weeks {
|
||||
newEvent := event
|
||||
newEvent.Week = week
|
||||
newEvents = append(newEvents, newEvent)
|
||||
}
|
||||
} else {
|
||||
newEvents = append(newEvents, event)
|
||||
}
|
||||
}
|
||||
return newEvents
|
||||
}
|
||||
|
||||
func splitWeekRange(weekRange string) []string {
|
||||
parts := strings.Split(weekRange, "-")
|
||||
if len(parts) != 2 {
|
||||
return nil // Invalid format
|
||||
}
|
||||
|
||||
start, errStart := strconv.Atoi(strings.TrimSpace(parts[0]))
|
||||
end, errEnd := strconv.Atoi(strings.TrimSpace(parts[1]))
|
||||
|
||||
if errStart != nil || errEnd != nil {
|
||||
return nil // Error converting to integers
|
||||
}
|
||||
|
||||
var weeks []string
|
||||
for i := start; i <= end; i++ {
|
||||
weeks = append(weeks, strconv.Itoa(i))
|
||||
}
|
||||
|
||||
return weeks
|
||||
}
|
@@ -0,0 +1,503 @@
|
||||
//Calendar implementation for the HTWK Leipzig timetable. Evaluation and display of the individual dates in iCal format.
|
||||
//Copyright (C) 2024 HTWKalender support@htwkalender.de
|
||||
|
||||
//This program is free software: you can redistribute it and/or modify
|
||||
//it under the terms of the GNU Affero General Public License as published by
|
||||
//the Free Software Foundation, either version 3 of the License, or
|
||||
//(at your option) any later version.
|
||||
|
||||
//This program is distributed in the hope that it will be useful,
|
||||
//but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
//MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
//GNU Affero General Public License for more details.
|
||||
|
||||
//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/>.
|
||||
|
||||
package v1
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/pocketbase/pocketbase/tools/types"
|
||||
"htwkalender/model"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func Test_extractSemesterAndYear(t *testing.T) {
|
||||
type args struct {
|
||||
semesterString string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want string
|
||||
want1 string
|
||||
}{
|
||||
{
|
||||
name: "Test 1",
|
||||
args: args{
|
||||
semesterString: "Wintersemester 2023/24 (Planungszeitraum 01.09.2023 bis 03.03.2024)",
|
||||
},
|
||||
want: "ws",
|
||||
want1: "2023",
|
||||
},
|
||||
{
|
||||
name: "Test 2",
|
||||
args: args{
|
||||
semesterString: "Sommersemester 2023 (Planungszeitraum 06.03. bis 31.08.2023)",
|
||||
},
|
||||
want: "ss",
|
||||
want1: "2023",
|
||||
},
|
||||
{
|
||||
name: "Test 3",
|
||||
args: args{
|
||||
semesterString: "Sommersemester 2010 (Planungszeitraum 06.03. bis 31.08.2023)",
|
||||
},
|
||||
want: "ss",
|
||||
want1: "2010",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, got1 := extractSemesterAndYear(tt.args.semesterString)
|
||||
if got != tt.want {
|
||||
t.Errorf("extractSemesterAndYear() got = %v, want %v", got, tt.want)
|
||||
}
|
||||
if got1 != tt.want1 {
|
||||
t.Errorf("extractSemesterAndYear() got1 = %v, want %v", got1, tt.want1)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_replaceEmptyEventNames(t *testing.T) {
|
||||
type args struct {
|
||||
group model.SeminarGroup
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want model.SeminarGroup
|
||||
}{
|
||||
{
|
||||
name: "Test 1",
|
||||
args: args{
|
||||
group: model.SeminarGroup{
|
||||
Events: []model.Event{
|
||||
{
|
||||
Name: "Test",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
want: model.SeminarGroup{
|
||||
Events: []model.Event{
|
||||
{
|
||||
Name: "Test",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Test 1",
|
||||
args: args{
|
||||
group: model.SeminarGroup{
|
||||
Events: []model.Event{
|
||||
{
|
||||
Name: "",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
want: model.SeminarGroup{
|
||||
Events: []model.Event{
|
||||
{
|
||||
Name: "Sonderveranstaltungen",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := ReplaceEmptyEventNames(tt.args.group); !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("ReplaceEmptyEventNames() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_splitEventType(t *testing.T) {
|
||||
type args struct {
|
||||
events []model.Event
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want []model.Event
|
||||
}{
|
||||
{
|
||||
name: "Test 1",
|
||||
args: args{
|
||||
events: []model.Event{
|
||||
{
|
||||
EventType: "V",
|
||||
},
|
||||
},
|
||||
},
|
||||
want: []model.Event{
|
||||
{
|
||||
EventType: "V",
|
||||
Compulsory: "",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Test 2",
|
||||
args: args{
|
||||
events: []model.Event{
|
||||
{
|
||||
EventType: "Vw",
|
||||
},
|
||||
},
|
||||
},
|
||||
want: []model.Event{
|
||||
{
|
||||
EventType: "V",
|
||||
Compulsory: "w",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Test 3",
|
||||
args: args{
|
||||
events: []model.Event{
|
||||
{
|
||||
EventType: "Sperr",
|
||||
},
|
||||
},
|
||||
},
|
||||
want: []model.Event{
|
||||
{
|
||||
EventType: "Sperr",
|
||||
Compulsory: "",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Test 4",
|
||||
args: args{
|
||||
events: []model.Event{
|
||||
{
|
||||
EventType: "Sperr",
|
||||
},
|
||||
{
|
||||
EventType: "Vw",
|
||||
},
|
||||
},
|
||||
},
|
||||
want: []model.Event{
|
||||
{
|
||||
EventType: "Sperr",
|
||||
Compulsory: "",
|
||||
},
|
||||
{
|
||||
EventType: "V",
|
||||
Compulsory: "w",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got, _ := SplitEventType(tt.args.events); !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("SplitEventType() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_generateUUIDs(t *testing.T) {
|
||||
type args struct {
|
||||
events []model.Event
|
||||
course string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want []model.Event
|
||||
}{
|
||||
{
|
||||
name: "Test 1",
|
||||
args: args{
|
||||
events: []model.Event{
|
||||
{
|
||||
Name: " Arbeitssicherheit / Rechtsformen von Unternehmen B435 SBB (wpf) & B348 BIB (pf) 5. FS",
|
||||
},
|
||||
},
|
||||
course: "21BIB-2a",
|
||||
},
|
||||
want: []model.Event{
|
||||
{
|
||||
Name: " Arbeitssicherheit / Rechtsformen von Unternehmen B435 SBB (wpf) & B348 BIB (pf) 5. FS",
|
||||
UUID: "3720afdc-10c7-5b72-9489-cffb70cb0c13",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Test 2",
|
||||
args: args{
|
||||
events: []model.Event{
|
||||
{
|
||||
Name: " Arbeitssicherheit / Rechtsformen von Unternehmen B435 SBB (wpf) & B348 BIB (pf) 5. FS",
|
||||
},
|
||||
},
|
||||
course: "21BIB-2b",
|
||||
},
|
||||
want: []model.Event{
|
||||
{
|
||||
Name: " Arbeitssicherheit / Rechtsformen von Unternehmen B435 SBB (wpf) & B348 BIB (pf) 5. FS",
|
||||
UUID: "81083480-bcf1-5452-af84-bb27d79282d8",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := generateUUIDs(tt.args.events, tt.args.course); !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("generateUUIDs() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_createTimeFromHourAndMinuteString(t *testing.T) {
|
||||
type args struct {
|
||||
tableTime string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want time.Time
|
||||
}{
|
||||
{
|
||||
name: "Test 1",
|
||||
args: args{
|
||||
tableTime: "08:00",
|
||||
},
|
||||
want: time.Date(0, 0, 0, 8, 0, 0, 0, time.UTC),
|
||||
},
|
||||
{
|
||||
name: "Test 2",
|
||||
args: args{
|
||||
tableTime: "08:15",
|
||||
},
|
||||
want: time.Date(0, 0, 0, 8, 15, 0, 0, time.UTC),
|
||||
},
|
||||
{
|
||||
name: "Test 3",
|
||||
args: args{
|
||||
tableTime: "08:30",
|
||||
},
|
||||
want: time.Date(0, 0, 0, 8, 30, 0, 0, time.UTC),
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := createTimeFromHourAndMinuteString(tt.args.tableTime); !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("createTimeFromHourAndMinuteString() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_replaceTimeInDate(t *testing.T) {
|
||||
type args struct {
|
||||
date time.Time
|
||||
time time.Time
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want time.Time
|
||||
}{
|
||||
{
|
||||
name: "Test 1",
|
||||
args: args{
|
||||
date: time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC),
|
||||
time: time.Date(0, 0, 0, 8, 0, 0, 0, time.UTC),
|
||||
},
|
||||
want: time.Date(2021, 1, 1, 8, 0, 0, 0, time.UTC),
|
||||
},
|
||||
{
|
||||
name: "Test 2",
|
||||
args: args{
|
||||
date: time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC),
|
||||
time: time.Date(0, 0, 0, 8, 15, 0, 0, time.UTC),
|
||||
},
|
||||
want: time.Date(2021, 1, 1, 8, 15, 0, 0, time.UTC),
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := replaceTimeForDate(tt.args.date, tt.args.time); !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("addTimeToDate() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_convertWeeksToDates(t *testing.T) {
|
||||
type args struct {
|
||||
events []model.Event
|
||||
semester string
|
||||
year string
|
||||
}
|
||||
returnDateTime := func(date time.Time) types.DateTime {
|
||||
dateTime, err := types.ParseDateTime(date)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
}
|
||||
return dateTime
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want []model.Event
|
||||
}{
|
||||
{
|
||||
name: "Test Wintertime",
|
||||
args: args{
|
||||
events: []model.Event{
|
||||
{
|
||||
Week: "1",
|
||||
Day: "Montag",
|
||||
Start: returnDateTime(time.Date(0, 0, 0, 7, 30, 0, 0, time.UTC)),
|
||||
End: returnDateTime(time.Date(0, 0, 0, 9, 0, 0, 0, time.UTC)),
|
||||
},
|
||||
},
|
||||
semester: "ws",
|
||||
year: "2021",
|
||||
},
|
||||
want: []model.Event{
|
||||
{
|
||||
Week: "1",
|
||||
Day: "Montag",
|
||||
Start: returnDateTime(time.Date(2021, 1, 4, 6, 30, 0, 0, time.UTC)),
|
||||
End: returnDateTime(time.Date(2021, 1, 4, 8, 0, 0, 0, time.UTC)),
|
||||
Semester: "ws",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Test Summertime",
|
||||
args: args{
|
||||
events: []model.Event{
|
||||
{
|
||||
Week: "30",
|
||||
Day: "Donnerstag",
|
||||
Start: returnDateTime(time.Date(0, 0, 0, 7, 30, 0, 0, time.UTC)),
|
||||
End: returnDateTime(time.Date(0, 0, 0, 0, 0, 0, 0, time.UTC)),
|
||||
},
|
||||
},
|
||||
semester: "ws",
|
||||
year: "2023",
|
||||
},
|
||||
want: []model.Event{
|
||||
{
|
||||
Week: "30",
|
||||
Day: "Donnerstag",
|
||||
Start: returnDateTime(time.Date(2023, 7, 27, 5, 30, 0, 0, time.UTC)),
|
||||
End: returnDateTime(time.Date(2023, 7, 27, 22, 0, 0, 0, time.UTC)),
|
||||
Semester: "ws",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Test NextDay",
|
||||
args: args{
|
||||
events: []model.Event{
|
||||
{
|
||||
Week: "45",
|
||||
Day: "Donnerstag",
|
||||
Start: returnDateTime(time.Date(0, 0, 0, 7, 30, 0, 0, time.UTC)),
|
||||
End: returnDateTime(time.Date(0, 0, 0, 4, 0, 0, 0, time.UTC)),
|
||||
},
|
||||
},
|
||||
semester: "ws",
|
||||
year: "2023",
|
||||
},
|
||||
want: []model.Event{
|
||||
{
|
||||
Week: "45",
|
||||
Day: "Donnerstag",
|
||||
Start: returnDateTime(time.Date(2023, 11, 9, 6, 30, 0, 0, time.UTC)),
|
||||
End: returnDateTime(time.Date(2023, 11, 10, 3, 0, 0, 0, time.UTC)),
|
||||
Semester: "ws",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := convertWeeksToDates(tt.args.events, tt.args.semester, tt.args.year); !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("convertWeeksToDates() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_replaceTimeForDate(t *testing.T) {
|
||||
type args struct {
|
||||
date time.Time
|
||||
replacementTime time.Time
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want time.Time
|
||||
}{
|
||||
{
|
||||
name: "Replace Hour",
|
||||
args: args{
|
||||
date: time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC),
|
||||
replacementTime: time.Date(0, 0, 0, 8, 0, 0, 0, time.UTC),
|
||||
},
|
||||
want: time.Date(2021, 1, 1, 8, 0, 0, 0, time.UTC),
|
||||
},
|
||||
{
|
||||
name: "Replace Hour and Minute",
|
||||
args: args{
|
||||
date: time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC),
|
||||
replacementTime: time.Date(0, 0, 0, 8, 15, 0, 0, time.UTC),
|
||||
},
|
||||
want: time.Date(2021, 1, 1, 8, 15, 0, 0, time.UTC),
|
||||
},
|
||||
{
|
||||
name: "Replace Hour and Minute",
|
||||
args: args{
|
||||
date: time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC),
|
||||
replacementTime: time.Date(0, 0, 0, 8, 30, 0, 0, time.UTC),
|
||||
},
|
||||
want: time.Date(2021, 1, 1, 8, 30, 0, 0, time.UTC),
|
||||
},
|
||||
{
|
||||
name: "Replace Hour and Minute without Year, Month, Day",
|
||||
args: args{
|
||||
date: time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC),
|
||||
replacementTime: time.Date(2023, 10, 3, 8, 30, 0, 0, time.UTC),
|
||||
},
|
||||
want: time.Date(2021, 1, 1, 8, 30, 0, 0, time.UTC),
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := replaceTimeForDate(tt.args.date, tt.args.replacementTime); !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("replaceTimeForDate() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
@@ -0,0 +1,141 @@
|
||||
//Calendar implementation for the HTWK Leipzig timetable. Evaluation and display of the individual dates in iCal format.
|
||||
//Copyright (C) 2024 HTWKalender support@htwkalender.de
|
||||
|
||||
//This program is free software: you can redistribute it and/or modify
|
||||
//it under the terms of the GNU Affero General Public License as published by
|
||||
//the Free Software Foundation, either version 3 of the License, or
|
||||
//(at your option) any later version.
|
||||
|
||||
//This program is distributed in the hope that it will be useful,
|
||||
//but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
//MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
//GNU Affero General Public License for more details.
|
||||
|
||||
//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/>.
|
||||
|
||||
package v1
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"github.com/pocketbase/pocketbase"
|
||||
"github.com/pocketbase/pocketbase/models"
|
||||
"htwkalender/model"
|
||||
"htwkalender/service/db"
|
||||
"htwkalender/service/functions"
|
||||
"htwkalender/service/functions/time"
|
||||
"io"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func getSeminarHTML(semester string) (string, error) {
|
||||
url := "https://stundenplan.htwk-leipzig.de/stundenplan/xml/public/semgrp_" + semester + ".xml"
|
||||
|
||||
// Send GET request
|
||||
response, err := http.Get(url)
|
||||
if err != nil {
|
||||
fmt.Printf("Error occurred while making the request: %s\n", err.Error())
|
||||
return "", err
|
||||
}
|
||||
defer func(Body io.ReadCloser) {
|
||||
err := Body.Close()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}(response.Body)
|
||||
|
||||
// Read the response body
|
||||
body, err := io.ReadAll(response.Body)
|
||||
|
||||
if err != nil {
|
||||
fmt.Printf("Error occurred while reading the response: %s\n", err.Error())
|
||||
return "", err
|
||||
}
|
||||
|
||||
return string(body), err
|
||||
|
||||
}
|
||||
|
||||
func FetchSeminarGroups(app *pocketbase.PocketBase) ([]*models.Record, error) {
|
||||
var groups []model.SeminarGroup
|
||||
|
||||
semesterString := functions.CalculateSemesterList(time.RealClock{})
|
||||
var results [2]string
|
||||
var err error
|
||||
|
||||
for i, semester := range semesterString {
|
||||
results[i], err = getSeminarHTML(semester)
|
||||
if err != nil {
|
||||
slog.Error("Error while fetching seminar groups for: "+semester, "error", err)
|
||||
return nil, err
|
||||
}
|
||||
groups = append(groups, parseSeminarGroups(results[i], semester)...)
|
||||
}
|
||||
|
||||
// filter duplicates
|
||||
groups = removeDuplicates(groups)
|
||||
|
||||
collection, dbError := db.FindCollection(app, "groups")
|
||||
if dbError != nil {
|
||||
slog.Error("Error while searching collection groups", "error", dbError)
|
||||
return nil, err
|
||||
}
|
||||
var insertedGroups []*models.Record
|
||||
|
||||
insertedGroups, dbError = db.SaveGroups(groups, collection, app)
|
||||
if dbError != nil {
|
||||
slog.Error("Error while saving groups", "error", dbError)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return insertedGroups, nil
|
||||
}
|
||||
|
||||
func removeDuplicates(groups []model.SeminarGroup) []model.SeminarGroup {
|
||||
var uniqueGroups []model.SeminarGroup
|
||||
for _, group := range groups {
|
||||
if !contains(uniqueGroups, group) {
|
||||
uniqueGroups = append(uniqueGroups, group)
|
||||
}
|
||||
}
|
||||
return uniqueGroups
|
||||
}
|
||||
|
||||
func contains(groups []model.SeminarGroup, group model.SeminarGroup) bool {
|
||||
for _, a := range groups {
|
||||
if (a.Course == group.Course) && (a.Semester == group.Semester) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func parseSeminarGroups(result string, semester string) []model.SeminarGroup {
|
||||
|
||||
var studium model.Studium
|
||||
err := xml.Unmarshal([]byte(result), &studium)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var seminarGroups []model.SeminarGroup
|
||||
for _, faculty := range studium.Faculty {
|
||||
for _, Studiengang := range faculty.Studiengang {
|
||||
for _, Studienrichtung := range Studiengang.Semgrp {
|
||||
seminarGroup := model.SeminarGroup{
|
||||
University: "HTWK-Leipzig",
|
||||
GroupShortcut: Studiengang.Name,
|
||||
GroupId: Studiengang.ID,
|
||||
Course: Studienrichtung.Name,
|
||||
Faculty: faculty.Name,
|
||||
FacultyId: faculty.ID,
|
||||
Semester: semester,
|
||||
}
|
||||
seminarGroups = append(seminarGroups, seminarGroup)
|
||||
}
|
||||
}
|
||||
}
|
||||
return seminarGroups
|
||||
}
|
@@ -0,0 +1,91 @@
|
||||
//Calendar implementation for the HTWK Leipzig timetable. Evaluation and display of the individual dates in iCal format.
|
||||
//Copyright (C) 2024 HTWKalender support@htwkalender.de
|
||||
|
||||
//This program is free software: you can redistribute it and/or modify
|
||||
//it under the terms of the GNU Affero General Public License as published by
|
||||
//the Free Software Foundation, either version 3 of the License, or
|
||||
//(at your option) any later version.
|
||||
|
||||
//This program is distributed in the hope that it will be useful,
|
||||
//but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
//MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
//GNU Affero General Public License for more details.
|
||||
|
||||
//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/>.
|
||||
|
||||
package v1
|
||||
|
||||
import (
|
||||
"htwkalender/model"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func Test_contains(t *testing.T) {
|
||||
type args struct {
|
||||
groups []model.SeminarGroup
|
||||
group model.SeminarGroup
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "should return true if group is in groups",
|
||||
args: args{
|
||||
groups: []model.SeminarGroup{
|
||||
{
|
||||
Course: "test",
|
||||
Semester: "test",
|
||||
},
|
||||
},
|
||||
group: model.SeminarGroup{
|
||||
Course: "test",
|
||||
Semester: "test",
|
||||
},
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "should return false if group is not in groups",
|
||||
args: args{
|
||||
groups: []model.SeminarGroup{
|
||||
{
|
||||
Course: "test",
|
||||
Semester: "test",
|
||||
},
|
||||
},
|
||||
group: model.SeminarGroup{
|
||||
Course: "test",
|
||||
Semester: "test2",
|
||||
},
|
||||
},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "should return false if group is not in courses",
|
||||
args: args{
|
||||
groups: []model.SeminarGroup{
|
||||
{
|
||||
Course: "test3",
|
||||
Semester: "test",
|
||||
},
|
||||
},
|
||||
group: model.SeminarGroup{
|
||||
Course: "test",
|
||||
Semester: "test",
|
||||
},
|
||||
},
|
||||
want: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := contains(tt.args.groups, tt.args.group); got != tt.want {
|
||||
t.Errorf("contains() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
217
services/data-manager/service/fetch/v1/htmlParsingFunctions.go
Normal file
217
services/data-manager/service/fetch/v1/htmlParsingFunctions.go
Normal file
@@ -0,0 +1,217 @@
|
||||
//Calendar implementation for the HTWK Leipzig timetable. Evaluation and display of the individual dates in iCal format.
|
||||
//Copyright (C) 2024 HTWKalender support@htwkalender.de
|
||||
|
||||
//This program is free software: you can redistribute it and/or modify
|
||||
//it under the terms of the GNU Affero General Public License as published by
|
||||
//the Free Software Foundation, either version 3 of the License, or
|
||||
//(at your option) any later version.
|
||||
|
||||
//This program is distributed in the hope that it will be useful,
|
||||
//but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
//MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
//GNU Affero General Public License for more details.
|
||||
|
||||
//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/>.
|
||||
|
||||
package v1
|
||||
|
||||
import (
|
||||
"golang.org/x/net/html"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Find the first <table> element in the HTML document
|
||||
func findFirstTable(node *html.Node) *html.Node {
|
||||
if node.Type == html.ElementNode && node.Data == "table" {
|
||||
return node
|
||||
}
|
||||
// Traverse child nodes recursively
|
||||
for child := node.FirstChild; child != nil; child = child.NextSibling {
|
||||
found := findFirstTable(child)
|
||||
if found != nil {
|
||||
return found
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Find the first <span> element with the specified class attribute value
|
||||
func findFirstSpanWithClass(node *html.Node, classValue string) *html.Node {
|
||||
|
||||
// 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) {
|
||||
return node
|
||||
}
|
||||
}
|
||||
|
||||
// Traverse child nodes recursively
|
||||
for child := node.FirstChild; child != nil; child = child.NextSibling {
|
||||
found := findFirstSpanWithClass(child, classValue)
|
||||
if found != nil {
|
||||
return found
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Check if the specified element has the specified class attribute value
|
||||
func hasClassAttribute(node *html.Node, classValue string) bool {
|
||||
for _, attr := range node.Attr {
|
||||
if attr.Key == "class" && strings.Contains(attr.Val, classValue) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Get Tables with days
|
||||
func getEventTables(node *html.Node) [][]*html.Node {
|
||||
var eventTables [][]*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)
|
||||
}
|
||||
}
|
||||
return eventTables
|
||||
}
|
||||
|
||||
// Get Tables with days
|
||||
func getAllDayLabels(node *html.Node) []string {
|
||||
paragraphs := findParagraphs(node)
|
||||
var dayArray []string
|
||||
|
||||
for _, p := range paragraphs {
|
||||
label := getDayLabel(p)
|
||||
if label != "" {
|
||||
dayArray = append(dayArray, label)
|
||||
}
|
||||
}
|
||||
return dayArray
|
||||
}
|
||||
|
||||
// Find all <p> elements in the HTML document
|
||||
func findParagraphs(node *html.Node) []*html.Node {
|
||||
var paragraphs []*html.Node
|
||||
|
||||
if node.Type == html.ElementNode && node.Data == "p" {
|
||||
paragraphs = append(paragraphs, node)
|
||||
}
|
||||
|
||||
for child := node.FirstChild; child != nil; child = child.NextSibling {
|
||||
paragraphs = append(paragraphs, findParagraphs(child)...)
|
||||
}
|
||||
|
||||
return paragraphs
|
||||
}
|
||||
|
||||
// Find all <tr> elements in <tbody>, excluding the first one
|
||||
func findTableRows(node *html.Node) []*html.Node {
|
||||
var tableRows []*html.Node
|
||||
|
||||
if node.Type == html.ElementNode && node.Data == "tbody" {
|
||||
child := node.FirstChild
|
||||
for child != nil {
|
||||
if child.Type == html.ElementNode && child.Data == "tr" {
|
||||
tableRows = append(tableRows, child)
|
||||
}
|
||||
child = child.NextSibling
|
||||
}
|
||||
}
|
||||
|
||||
// Traverse child nodes recursively
|
||||
for child := node.FirstChild; child != nil; child = child.NextSibling {
|
||||
var tableRowElement = findTableRows(child)
|
||||
if tableRowElement != nil {
|
||||
tableRows = append(tableRows, tableRowElement...)
|
||||
}
|
||||
}
|
||||
|
||||
// check if tableRows is nil
|
||||
if tableRows == nil {
|
||||
return []*html.Node{}
|
||||
} else {
|
||||
return tableRows
|
||||
}
|
||||
}
|
||||
|
||||
// Find all <p> elements in the HTML document
|
||||
func findTables(node *html.Node) []*html.Node {
|
||||
var tables []*html.Node
|
||||
|
||||
if node.Type == html.ElementNode && node.Data == "table" {
|
||||
tables = append(tables, node)
|
||||
}
|
||||
|
||||
for child := node.FirstChild; child != nil; child = child.NextSibling {
|
||||
tables = append(tables, findDayTables(child)...)
|
||||
}
|
||||
|
||||
return tables
|
||||
}
|
||||
|
||||
// Find all <p> elements in the HTML document
|
||||
func findDayTables(node *html.Node) []*html.Node {
|
||||
var tables []*html.Node
|
||||
|
||||
for child := node.FirstChild; child != nil; child = child.NextSibling {
|
||||
tables = append(tables, findDayTables(child)...)
|
||||
}
|
||||
|
||||
if node.Type == html.ElementNode && node.Data == "table" && hasClassAttribute(node, "spreadsheet") {
|
||||
tables = append(tables, node)
|
||||
}
|
||||
|
||||
return tables
|
||||
}
|
||||
|
||||
// Get the text content of the specified node and its descendants
|
||||
func getDayLabel(node *html.Node) string {
|
||||
|
||||
child := node.FirstChild
|
||||
if child != nil {
|
||||
if child.Type == html.ElementNode && child.Data == "span" {
|
||||
if child.FirstChild != nil {
|
||||
return child.FirstChild.Data
|
||||
}
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// Find all <td> elements in the current <tr>
|
||||
func findTableData(node *html.Node) []*html.Node {
|
||||
var tableData []*html.Node
|
||||
|
||||
if node.Type == html.ElementNode && node.Data == "tr" {
|
||||
child := node.FirstChild
|
||||
for child != nil {
|
||||
if child.Type == html.ElementNode && child.Data == "td" {
|
||||
tableData = append(tableData, child)
|
||||
}
|
||||
child = child.NextSibling
|
||||
}
|
||||
}
|
||||
return tableData
|
||||
}
|
||||
|
||||
// Get the text content of the specified node and its descendants
|
||||
func getTextContent(node *html.Node) string {
|
||||
var textContent string
|
||||
|
||||
if node.Type == html.TextNode {
|
||||
textContent = node.Data
|
||||
}
|
||||
|
||||
for child := node.FirstChild; child != nil; child = child.NextSibling {
|
||||
textContent += getTextContent(child)
|
||||
}
|
||||
|
||||
return textContent
|
||||
}
|
67
services/data-manager/service/fetch/v2/eventParser.go
Normal file
67
services/data-manager/service/fetch/v2/eventParser.go
Normal file
@@ -0,0 +1,67 @@
|
||||
//Calendar implementation for the HTWK Leipzig timetable. Evaluation and display of the individual dates in iCal format.
|
||||
//Copyright (C) 2024 HTWKalender support@htwkalender.de
|
||||
|
||||
//This program is free software: you can redistribute it and/or modify
|
||||
//it under the terms of the GNU Affero General Public License as published by
|
||||
//the Free Software Foundation, either version 3 of the License, or
|
||||
//(at your option) any later version.
|
||||
|
||||
//This program is distributed in the hope that it will be useful,
|
||||
//but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
//MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
//GNU Affero General Public License for more details.
|
||||
|
||||
//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/>.
|
||||
|
||||
package v2
|
||||
|
||||
import (
|
||||
"github.com/pocketbase/pocketbase/tools/types"
|
||||
"golang.org/x/net/html"
|
||||
"htwkalender/model"
|
||||
"htwkalender/service/date"
|
||||
"htwkalender/service/functions"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func toEvents(tables [][]*html.Node, days []string) []model.Event {
|
||||
var events []model.Event
|
||||
|
||||
for table := range tables {
|
||||
for row := range tables[table] {
|
||||
|
||||
tableData := findTableData(tables[table][row])
|
||||
if len(tableData) > 0 {
|
||||
start, _ := types.ParseDateTime(date.CreateTimeFromHourAndMinuteString(getTextContent(tableData[1])))
|
||||
end, _ := types.ParseDateTime(date.CreateTimeFromHourAndMinuteString(getTextContent(tableData[2])))
|
||||
|
||||
courses := getTextContent(tableData[7])
|
||||
name := getTextContent(tableData[3])
|
||||
if functions.OnlyWhitespace(name) {
|
||||
name = "Sonderveranstaltung"
|
||||
}
|
||||
|
||||
if len(courses) > 0 {
|
||||
for _, course := range strings.Split(courses, " ") {
|
||||
events = append(events, model.Event{
|
||||
Day: days[table],
|
||||
Week: getTextContent(tableData[0]),
|
||||
Start: start,
|
||||
End: end,
|
||||
Name: name,
|
||||
EventType: getTextContent(tableData[4]),
|
||||
Notes: getTextContent(tableData[5]),
|
||||
Prof: getTextContent(tableData[6]),
|
||||
Rooms: getTextContent(tableData[8]),
|
||||
BookedAt: getTextContent(tableData[10]),
|
||||
Course: strings.TrimSpace(course),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
return events
|
||||
}
|
181
services/data-manager/service/fetch/v2/fetcher.go
Normal file
181
services/data-manager/service/fetch/v2/fetcher.go
Normal file
@@ -0,0 +1,181 @@
|
||||
//Calendar implementation for the HTWK Leipzig timetable. Evaluation and display of the individual dates in iCal format.
|
||||
//Copyright (C) 2024 HTWKalender support@htwkalender.de
|
||||
|
||||
//This program is free software: you can redistribute it and/or modify
|
||||
//it under the terms of the GNU Affero General Public License as published by
|
||||
//the Free Software Foundation, either version 3 of the License, or
|
||||
//(at your option) any later version.
|
||||
|
||||
//This program is distributed in the hope that it will be useful,
|
||||
//but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
//MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
//GNU Affero General Public License for more details.
|
||||
|
||||
//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/>.
|
||||
|
||||
package v2
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/google/uuid"
|
||||
"github.com/pocketbase/pocketbase"
|
||||
"golang.org/x/net/html"
|
||||
"htwkalender/model"
|
||||
"htwkalender/service/db"
|
||||
"htwkalender/service/fetch"
|
||||
v1 "htwkalender/service/fetch/v1"
|
||||
"htwkalender/service/functions"
|
||||
localTime "htwkalender/service/functions/time"
|
||||
"log/slog"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func ParseEventsFromRemote(app *pocketbase.PocketBase) (model.Events, error) {
|
||||
savedRecords, err := FetchAllEventsAndSave(app, localTime.RealClock{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return savedRecords, nil
|
||||
}
|
||||
|
||||
func FetchAllEventsAndSave(app *pocketbase.PocketBase, clock localTime.Clock) ([]model.Event, error) {
|
||||
var savedRecords []model.Event
|
||||
var err error = nil
|
||||
|
||||
var stubUrl = [2]string{
|
||||
"https://stundenplan.htwk-leipzig.de/",
|
||||
"/Berichte/Text-Listen;Veranstaltungsarten;name;" +
|
||||
"Vp%0A" +
|
||||
"Vw%0A" +
|
||||
"V%0A" +
|
||||
"Sp%0A" +
|
||||
"Sw%0A" +
|
||||
"S%0A" +
|
||||
"Pp%0A" +
|
||||
"Pw%0A" +
|
||||
"P%0A" +
|
||||
"ZV%0A" +
|
||||
"Tut%0A" +
|
||||
"Sperr%0A" +
|
||||
"pf%0A" +
|
||||
"wpf%0A" +
|
||||
"fak%0A" +
|
||||
"Pruefung%0A" +
|
||||
"gebucht%0A" +
|
||||
"Vertretung%0A" +
|
||||
"Fremdveranst.%0A" +
|
||||
"Buchen%0A" +
|
||||
"%0A?&template=sws_modul&weeks=1-65&combined=yes",
|
||||
}
|
||||
|
||||
// Fetch and save events for all semesters
|
||||
for _, semester := range functions.CalculateSemesterList(clock) {
|
||||
events, fetchErr := fetchAndSaveAllEventsForSemester(app, semester, stubUrl)
|
||||
if fetchErr != nil {
|
||||
return nil, fmt.Errorf("failed to fetch and save events for "+semester+": %w", err)
|
||||
}
|
||||
savedRecords = append(savedRecords, events...)
|
||||
}
|
||||
|
||||
return savedRecords, err
|
||||
}
|
||||
|
||||
func fetchAndSaveAllEventsForSemester(
|
||||
app *pocketbase.PocketBase,
|
||||
semester string,
|
||||
stubUrl [2]string,
|
||||
) ([]model.Event, error) {
|
||||
var savedRecords []model.Event
|
||||
url := stubUrl[0] + semester + stubUrl[1]
|
||||
events, err := parseEventForOneSemester(url)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse events for "+semester+": %w", err)
|
||||
}
|
||||
err = db.DeleteAllEventsBySemesterWithoutCourse(app, "Sport", semester)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to delete all events for "+semester+": %w", err)
|
||||
}
|
||||
savedEvents, dbError := db.SaveEvents(events, app)
|
||||
if dbError != nil {
|
||||
return nil, fmt.Errorf("failed to save events for "+semester+": %w", dbError)
|
||||
}
|
||||
savedRecords = append(savedRecords, savedEvents...)
|
||||
return savedRecords, err
|
||||
}
|
||||
|
||||
func parseEventForOneSemester(url string) ([]model.Event, error) {
|
||||
// Fetch Webpage from URL
|
||||
webpage, err := fetch.GetHTML(url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Parse HTML to Node Tree
|
||||
var doc *html.Node
|
||||
doc, err = parseHTML(webpage)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Get all event tables and all day labels
|
||||
eventTables := getEventTables(doc)
|
||||
allDayLabels := getAllDayLabels(doc)
|
||||
eventsWithCombinedWeeks := toEvents(eventTables, allDayLabels)
|
||||
|
||||
splitEventsByWeekVal := splitEventsByWeek(eventsWithCombinedWeeks)
|
||||
events := splitEventsBySingleWeek(splitEventsByWeekVal)
|
||||
|
||||
if events == nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
table := findFirstTable(doc)
|
||||
|
||||
if table == nil {
|
||||
return nil, fmt.Errorf("failed to find first table")
|
||||
}
|
||||
|
||||
semesterString := findFirstSpanWithClass(table, "header-0-2-0").FirstChild.Data
|
||||
semester, year := extractSemesterAndYear(semesterString)
|
||||
events = convertWeeksToDates(events, semester, year)
|
||||
events, err = v1.SplitEventType(events)
|
||||
if err != nil {
|
||||
slog.Error("Error occurred while splitting event types: ", "error", err)
|
||||
return nil, err
|
||||
}
|
||||
events = switchNameAndNotesForExam(events)
|
||||
events = generateUUIDs(events)
|
||||
|
||||
return events, nil
|
||||
}
|
||||
|
||||
// switch name and notes for Pruefung events when Note is not empty and Name starts with "Prüfungen" and contains email
|
||||
func switchNameAndNotesForExam(events []model.Event) []model.Event {
|
||||
for i, event := range events {
|
||||
if event.EventType == "Pruefung" {
|
||||
if event.Notes != "" && strings.HasPrefix(event.Name, "Prüfungen") && strings.Contains(event.Name, "@") {
|
||||
events[i].Name = event.Notes
|
||||
events[i].Notes = event.Name
|
||||
}
|
||||
}
|
||||
}
|
||||
return events
|
||||
}
|
||||
|
||||
func parseHTML(webpage string) (*html.Node, error) {
|
||||
doc, err := html.Parse(strings.NewReader(webpage))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return doc, nil
|
||||
}
|
||||
|
||||
func generateUUIDs(events []model.Event) []model.Event {
|
||||
for i, event := range events {
|
||||
// generate a hash value from the event name, course and semester
|
||||
hash := uuid.NewSHA1(uuid.NameSpaceOID, []byte(event.Name+event.Course))
|
||||
events[i].UUID = hash.String()
|
||||
}
|
||||
return events
|
||||
}
|
99
services/data-manager/service/fetch/v2/fetcher_test.go
Normal file
99
services/data-manager/service/fetch/v2/fetcher_test.go
Normal file
@@ -0,0 +1,99 @@
|
||||
//Calendar implementation for the HTWK Leipzig timetable. Evaluation and display of the individual dates in iCal format.
|
||||
//Copyright (C) 2024 HTWKalender support@htwkalender.de
|
||||
|
||||
//This program is free software: you can redistribute it and/or modify
|
||||
//it under the terms of the GNU Affero General Public License as published by
|
||||
//the Free Software Foundation, either version 3 of the License, or
|
||||
//(at your option) any later version.
|
||||
|
||||
//This program is distributed in the hope that it will be useful,
|
||||
//but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
//MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
//GNU Affero General Public License for more details.
|
||||
|
||||
//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/>.
|
||||
|
||||
package v2
|
||||
|
||||
import (
|
||||
"htwkalender/model"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func Test_switchNameAndNotesForExam(t *testing.T) {
|
||||
type args struct {
|
||||
events []model.Event
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want []model.Event
|
||||
}{
|
||||
{
|
||||
name: "switch name and notes for exam",
|
||||
args: args{
|
||||
events: []model.Event{
|
||||
{
|
||||
EventType: "Pruefung",
|
||||
Name: "Prüfungen FING/EIT WiSe (pruefungsamt.fing-eit@htwk-leipzig.de)",
|
||||
Notes: "Computer Vision II - Räume/Zeit unter Vorbehalt- (Raum W111.1)",
|
||||
},
|
||||
},
|
||||
},
|
||||
want: []model.Event{
|
||||
{
|
||||
EventType: "Pruefung",
|
||||
Name: "Computer Vision II - Räume/Zeit unter Vorbehalt- (Raum W111.1)",
|
||||
Notes: "Prüfungen FING/EIT WiSe (pruefungsamt.fing-eit@htwk-leipzig.de)",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "dont switch name and notes for exam",
|
||||
args: args{
|
||||
events: []model.Event{
|
||||
{
|
||||
EventType: "Pruefung",
|
||||
Name: "i054 Umweltschutz und Recycling DPB & VNB 7.FS (wpf)",
|
||||
Notes: "Prüfung",
|
||||
},
|
||||
},
|
||||
},
|
||||
want: []model.Event{
|
||||
{
|
||||
EventType: "Pruefung",
|
||||
Notes: "Prüfung",
|
||||
Name: "i054 Umweltschutz und Recycling DPB & VNB 7.FS (wpf)",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "dont switch name and notes for exam",
|
||||
args: args{
|
||||
events: []model.Event{
|
||||
{
|
||||
EventType: "Pruefung",
|
||||
Name: "Prüfungen FING/ME WiSe (pruefungsamt.fing-me@htwk-leipzig.de)",
|
||||
Notes: "",
|
||||
},
|
||||
},
|
||||
},
|
||||
want: []model.Event{
|
||||
{
|
||||
EventType: "Pruefung",
|
||||
Notes: "",
|
||||
Name: "Prüfungen FING/ME WiSe (pruefungsamt.fing-me@htwk-leipzig.de)",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := switchNameAndNotesForExam(tt.args.events); !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("switchNameAndNotesForExam() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
340
services/data-manager/service/fetch/v2/htmlParsingFunctions.go
Normal file
340
services/data-manager/service/fetch/v2/htmlParsingFunctions.go
Normal file
@@ -0,0 +1,340 @@
|
||||
//Calendar implementation for the HTWK Leipzig timetable. Evaluation and display of the individual dates in iCal format.
|
||||
//Copyright (C) 2024 HTWKalender support@htwkalender.de
|
||||
|
||||
//This program is free software: you can redistribute it and/or modify
|
||||
//it under the terms of the GNU Affero General Public License as published by
|
||||
//the Free Software Foundation, either version 3 of the License, or
|
||||
//(at your option) any later version.
|
||||
|
||||
//This program is distributed in the hope that it will be useful,
|
||||
//but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
//MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
//GNU Affero General Public License for more details.
|
||||
|
||||
//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/>.
|
||||
|
||||
package v2
|
||||
|
||||
import (
|
||||
"github.com/pocketbase/pocketbase/tools/types"
|
||||
"golang.org/x/net/html"
|
||||
"htwkalender/model"
|
||||
"htwkalender/service/date"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Find the first <table> element in the HTML document
|
||||
func findFirstTable(node *html.Node) *html.Node {
|
||||
if node.Type == html.ElementNode && node.Data == "table" {
|
||||
return node
|
||||
}
|
||||
// Traverse child nodes recursively
|
||||
for child := node.FirstChild; child != nil; child = child.NextSibling {
|
||||
found := findFirstTable(child)
|
||||
if found != nil {
|
||||
return found
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Find the first <span> element with the specified class attribute value
|
||||
func findFirstSpanWithClass(node *html.Node, classValue string) *html.Node {
|
||||
|
||||
// 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) {
|
||||
return node
|
||||
}
|
||||
}
|
||||
|
||||
// Traverse child nodes recursively
|
||||
for child := node.FirstChild; child != nil; child = child.NextSibling {
|
||||
found := findFirstSpanWithClass(child, classValue)
|
||||
if found != nil {
|
||||
return found
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Check if the specified element has the specified class attribute value
|
||||
func hasClassAttribute(node *html.Node, classValue string) bool {
|
||||
for _, attr := range node.Attr {
|
||||
if attr.Key == "class" && strings.Contains(attr.Val, classValue) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Get Tables with days
|
||||
func getEventTables(node *html.Node) [][]*html.Node {
|
||||
var eventTables [][]*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)
|
||||
}
|
||||
}
|
||||
return eventTables
|
||||
}
|
||||
|
||||
// Get Tables with days
|
||||
func getAllDayLabels(node *html.Node) []string {
|
||||
paragraphs := findParagraphs(node)
|
||||
var dayArray []string
|
||||
|
||||
for _, p := range paragraphs {
|
||||
label := getDayLabel(p)
|
||||
if label != "" {
|
||||
dayArray = append(dayArray, label)
|
||||
}
|
||||
}
|
||||
return dayArray
|
||||
}
|
||||
|
||||
// Find all <p> elements in the HTML document
|
||||
func findParagraphs(node *html.Node) []*html.Node {
|
||||
var paragraphs []*html.Node
|
||||
|
||||
if node.Type == html.ElementNode && node.Data == "p" {
|
||||
paragraphs = append(paragraphs, node)
|
||||
}
|
||||
|
||||
for child := node.FirstChild; child != nil; child = child.NextSibling {
|
||||
paragraphs = append(paragraphs, findParagraphs(child)...)
|
||||
}
|
||||
|
||||
return paragraphs
|
||||
}
|
||||
|
||||
// Find all <tr> elements in <tbody>, excluding the first one
|
||||
func findTableRows(node *html.Node) []*html.Node {
|
||||
var tableRows []*html.Node
|
||||
|
||||
if node.Type == html.ElementNode && node.Data == "tbody" {
|
||||
child := node.FirstChild
|
||||
for child != nil {
|
||||
if child.Type == html.ElementNode && child.Data == "tr" {
|
||||
tableRows = append(tableRows, child)
|
||||
}
|
||||
child = child.NextSibling
|
||||
}
|
||||
}
|
||||
|
||||
// Traverse child nodes recursively
|
||||
for child := node.FirstChild; child != nil; child = child.NextSibling {
|
||||
var tableRowElement = findTableRows(child)
|
||||
if tableRowElement != nil {
|
||||
tableRows = append(tableRows, tableRowElement...)
|
||||
}
|
||||
}
|
||||
|
||||
// check if tableRows is nil
|
||||
if tableRows == nil {
|
||||
return []*html.Node{}
|
||||
} else {
|
||||
return tableRows
|
||||
}
|
||||
}
|
||||
|
||||
// Find all <p> elements in the HTML document
|
||||
func findTables(node *html.Node) []*html.Node {
|
||||
var tables []*html.Node
|
||||
|
||||
if node.Type == html.ElementNode && node.Data == "table" {
|
||||
tables = append(tables, node)
|
||||
}
|
||||
|
||||
for child := node.FirstChild; child != nil; child = child.NextSibling {
|
||||
tables = append(tables, findDayTables(child)...)
|
||||
}
|
||||
|
||||
return tables
|
||||
}
|
||||
|
||||
// Find all <p> elements in the HTML document
|
||||
func findDayTables(node *html.Node) []*html.Node {
|
||||
var tables []*html.Node
|
||||
|
||||
for child := node.FirstChild; child != nil; child = child.NextSibling {
|
||||
tables = append(tables, findDayTables(child)...)
|
||||
}
|
||||
|
||||
if node.Type == html.ElementNode && node.Data == "table" && hasClassAttribute(node, "spreadsheet") {
|
||||
tables = append(tables, node)
|
||||
}
|
||||
|
||||
return tables
|
||||
}
|
||||
|
||||
// Get the text content of the specified node and its descendants
|
||||
func getDayLabel(node *html.Node) string {
|
||||
|
||||
child := node.FirstChild
|
||||
if child != nil {
|
||||
if child.Type == html.ElementNode && child.Data == "span" {
|
||||
if child.FirstChild != nil {
|
||||
return child.FirstChild.Data
|
||||
}
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// Find all <td> elements in the current <tr>
|
||||
func findTableData(node *html.Node) []*html.Node {
|
||||
var tableData []*html.Node
|
||||
|
||||
if node.Type == html.ElementNode && node.Data == "tr" {
|
||||
child := node.FirstChild
|
||||
for child != nil {
|
||||
if child.Type == html.ElementNode && child.Data == "td" {
|
||||
tableData = append(tableData, child)
|
||||
}
|
||||
child = child.NextSibling
|
||||
}
|
||||
}
|
||||
|
||||
return tableData
|
||||
}
|
||||
|
||||
// Get the text content of the specified node and its descendants
|
||||
func getTextContent(node *html.Node) string {
|
||||
var textContent string
|
||||
|
||||
if node.Type == html.TextNode {
|
||||
textContent = node.Data
|
||||
}
|
||||
|
||||
for child := node.FirstChild; child != nil; child = child.NextSibling {
|
||||
textContent += getTextContent(child)
|
||||
}
|
||||
|
||||
return textContent
|
||||
}
|
||||
|
||||
func splitEventsByWeek(events []model.Event) []model.Event {
|
||||
var newEvents []model.Event
|
||||
|
||||
for _, event := range events {
|
||||
weeks := strings.Split(event.Week, ",")
|
||||
for _, week := range weeks {
|
||||
newEvent := event
|
||||
newEvent.Week = strings.TrimSpace(week)
|
||||
newEvents = append(newEvents, newEvent)
|
||||
}
|
||||
}
|
||||
return newEvents
|
||||
}
|
||||
|
||||
func splitEventsBySingleWeek(events []model.Event) []model.Event {
|
||||
var newEvents []model.Event
|
||||
|
||||
for _, event := range events {
|
||||
if strings.Contains(event.Week, "-") {
|
||||
weeks := splitWeekRange(event.Week)
|
||||
for _, week := range weeks {
|
||||
newEvent := event
|
||||
newEvent.Week = week
|
||||
newEvents = append(newEvents, newEvent)
|
||||
}
|
||||
} else {
|
||||
newEvents = append(newEvents, event)
|
||||
}
|
||||
}
|
||||
return newEvents
|
||||
}
|
||||
|
||||
func splitWeekRange(weekRange string) []string {
|
||||
parts := strings.Split(weekRange, "-")
|
||||
if len(parts) != 2 {
|
||||
return nil // Invalid format
|
||||
}
|
||||
|
||||
start, errStart := strconv.Atoi(strings.TrimSpace(parts[0]))
|
||||
end, errEnd := strconv.Atoi(strings.TrimSpace(parts[1]))
|
||||
|
||||
if errStart != nil || errEnd != nil {
|
||||
return nil // Error converting to integers
|
||||
}
|
||||
|
||||
var weeks []string
|
||||
for i := start; i <= end; i++ {
|
||||
weeks = append(weeks, strconv.Itoa(i))
|
||||
}
|
||||
|
||||
return weeks
|
||||
}
|
||||
|
||||
func extractSemesterAndYear(semesterString string) (string, string) {
|
||||
winterPattern := "Wintersemester"
|
||||
summerPattern := "Sommersemester"
|
||||
|
||||
winterMatch := strings.Contains(semesterString, winterPattern)
|
||||
summerMatch := strings.Contains(semesterString, summerPattern)
|
||||
|
||||
semester := ""
|
||||
semesterShortcut := ""
|
||||
|
||||
if winterMatch {
|
||||
semester = "Wintersemester"
|
||||
semesterShortcut = "ws"
|
||||
} else if summerMatch {
|
||||
semester = "Sommersemester"
|
||||
semesterShortcut = "ss"
|
||||
} else {
|
||||
return "", ""
|
||||
}
|
||||
|
||||
yearPattern := `\d{4}`
|
||||
combinedPattern := semester + `\s` + yearPattern
|
||||
re := regexp.MustCompile(combinedPattern)
|
||||
match := re.FindString(semesterString)
|
||||
year := ""
|
||||
|
||||
if match != "" {
|
||||
reYear := regexp.MustCompile(yearPattern)
|
||||
year = reYear.FindString(match)
|
||||
}
|
||||
return semesterShortcut, year
|
||||
}
|
||||
|
||||
func convertWeeksToDates(events []model.Event, semester string, year string) []model.Event {
|
||||
var newEvents []model.Event
|
||||
eventYear, _ := strconv.Atoi(year)
|
||||
|
||||
// for each event we need to calculate the start and end date based on the week and the year
|
||||
for _, event := range events {
|
||||
eventWeek, _ := strconv.Atoi(event.Week)
|
||||
eventDay, _ := date.GetDateFromWeekNumber(eventYear, eventWeek, event.Day)
|
||||
start := replaceTimeForDate(eventDay, event.Start.Time())
|
||||
end := replaceTimeForDate(eventDay, event.End.Time())
|
||||
|
||||
//Check if end is before start
|
||||
if end.Before(start) {
|
||||
end = end.AddDate(0, 0, 1)
|
||||
}
|
||||
|
||||
newEvent := event
|
||||
newEvent.Start, _ = types.ParseDateTime(start.In(time.UTC))
|
||||
newEvent.End, _ = types.ParseDateTime(end.In(time.UTC))
|
||||
newEvent.Semester = semester
|
||||
newEvents = append(newEvents, newEvent)
|
||||
}
|
||||
return newEvents
|
||||
}
|
||||
|
||||
// replaceTimeForDate replaces hour, minute, second, nsec for the selected date
|
||||
func replaceTimeForDate(date time.Time, replacementTime time.Time) time.Time {
|
||||
return time.Date(date.Year(), date.Month(), date.Day(), replacementTime.Hour(), replacementTime.Minute(), replacementTime.Second(), replacementTime.Nanosecond(), date.Location())
|
||||
}
|
47
services/data-manager/service/functions/semester.go
Normal file
47
services/data-manager/service/functions/semester.go
Normal file
@@ -0,0 +1,47 @@
|
||||
//Calendar implementation for the HTWK Leipzig timetable. Evaluation and display of the individual dates in iCal format.
|
||||
//Copyright (C) 2024 HTWKalender support@htwkalender.de
|
||||
|
||||
//This program is free software: you can redistribute it and/or modify
|
||||
//it under the terms of the GNU Affero General Public License as published by
|
||||
//the Free Software Foundation, either version 3 of the License, or
|
||||
//(at your option) any later version.
|
||||
|
||||
//This program is distributed in the hope that it will be useful,
|
||||
//but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
//MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
//GNU Affero General Public License for more details.
|
||||
|
||||
//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/>.
|
||||
|
||||
package functions
|
||||
|
||||
import (
|
||||
localTime "htwkalender/service/functions/time"
|
||||
"time"
|
||||
)
|
||||
|
||||
// GetCurrentSemesterString returns the current semester as string
|
||||
// if current month is between 10 and 03 -> winter semester "ws"
|
||||
func GetCurrentSemesterString(localeTime localTime.Clock) string {
|
||||
if localeTime.Now().Month() >= 10 || localeTime.Now().Month() <= 3 {
|
||||
return "ws"
|
||||
} else {
|
||||
return "ss"
|
||||
}
|
||||
}
|
||||
|
||||
func CalculateSemesterList(clock localTime.Clock) []string {
|
||||
summerSemester := clock.Now().Month() >= time.March && clock.Now().Month() <= time.September
|
||||
winterSemester := clock.Now().Month() <= time.March || clock.Now().Month() >= time.September
|
||||
|
||||
if summerSemester && !winterSemester {
|
||||
return []string{"ss"}
|
||||
}
|
||||
|
||||
if !summerSemester && winterSemester {
|
||||
return []string{"ws"}
|
||||
}
|
||||
|
||||
return []string{"ss", "ws"}
|
||||
}
|
91
services/data-manager/service/functions/semester_test.go
Normal file
91
services/data-manager/service/functions/semester_test.go
Normal file
@@ -0,0 +1,91 @@
|
||||
package functions
|
||||
|
||||
import (
|
||||
mockTime "htwkalender/service/functions/time"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func Test_calculateSemesterList(t *testing.T) {
|
||||
type args struct {
|
||||
clock mockTime.Clock
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want []string
|
||||
}{
|
||||
{
|
||||
name: "is summer semester",
|
||||
args: args{
|
||||
clock: mockTime.MockClock{
|
||||
NowTime: time.Date(2024, 6, 1, 0, 0, 0, 0, time.UTC),
|
||||
},
|
||||
},
|
||||
want: []string{"ss"},
|
||||
},
|
||||
{
|
||||
name: "is winter semester",
|
||||
args: args{
|
||||
clock: mockTime.MockClock{
|
||||
NowTime: time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC),
|
||||
},
|
||||
},
|
||||
want: []string{"ws"},
|
||||
},
|
||||
{
|
||||
name: "is in both",
|
||||
args: args{
|
||||
clock: mockTime.MockClock{
|
||||
NowTime: time.Date(2024, 3, 22, 0, 0, 0, 0, time.UTC),
|
||||
},
|
||||
},
|
||||
want: []string{"ss", "ws"},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := CalculateSemesterList(tt.args.clock); !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("calculateSemesterList() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetCurrentSemesterString(t *testing.T) {
|
||||
type args struct {
|
||||
localeTime mockTime.Clock
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "is winter semester",
|
||||
args: args{
|
||||
localeTime: mockTime.MockClock{
|
||||
NowTime: time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC),
|
||||
},
|
||||
},
|
||||
want: "ws",
|
||||
},
|
||||
{
|
||||
name: "is summer semester",
|
||||
args: args{
|
||||
localeTime: mockTime.MockClock{
|
||||
NowTime: time.Date(2024, 6, 1, 0, 0, 0, 0, time.UTC),
|
||||
},
|
||||
},
|
||||
want: "ss",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := GetCurrentSemesterString(tt.args.localeTime); got != tt.want {
|
||||
t.Errorf("GetCurrentSemesterString() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
62
services/data-manager/service/functions/string.go
Normal file
62
services/data-manager/service/functions/string.go
Normal file
@@ -0,0 +1,62 @@
|
||||
//Calendar implementation for the HTWK Leipzig timetable. Evaluation and display of the individual dates in iCal format.
|
||||
//Copyright (C) 2024 HTWKalender support@htwkalender.de
|
||||
|
||||
//This program is free software: you can redistribute it and/or modify
|
||||
//it under the terms of the GNU Affero General Public License as published by
|
||||
//the Free Software Foundation, either version 3 of the License, or
|
||||
//(at your option) any later version.
|
||||
|
||||
//This program is distributed in the hope that it will be useful,
|
||||
//but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
//MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
//GNU Affero General Public License for more details.
|
||||
|
||||
//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/>.
|
||||
|
||||
package functions
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// check if string is empty or contains only whitespaces
|
||||
func OnlyWhitespace(word string) bool {
|
||||
return len(strings.TrimSpace(word)) == 0
|
||||
}
|
||||
|
||||
// return function to check if rune is a separator
|
||||
func IsSeparator(separator []rune) func(rune) bool {
|
||||
return func(character rune) bool {
|
||||
for _, sep := range separator {
|
||||
if sep == character {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func Contains(s []string, e string) bool {
|
||||
for _, a := range s {
|
||||
if a == e {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func HashString(s string) string {
|
||||
hash := sha256.New()
|
||||
hash.Write([]byte(s))
|
||||
hashInBytes := hash.Sum(nil)
|
||||
return hex.EncodeToString(hashInBytes)
|
||||
}
|
||||
|
||||
func SeperateRoomString(rooms string) []string {
|
||||
return strings.FieldsFunc(rooms, IsSeparator(
|
||||
[]rune{',', '\t', '\n', '\r', ';', ' ', '\u00A0'}),
|
||||
)
|
||||
}
|
145
services/data-manager/service/functions/string_test.go
Normal file
145
services/data-manager/service/functions/string_test.go
Normal file
@@ -0,0 +1,145 @@
|
||||
//Calendar implementation for the HTWK Leipzig timetable. Evaluation and display of the individual dates in iCal format.
|
||||
//Copyright (C) 2024 HTWKalender support@htwkalender.de
|
||||
|
||||
//This program is free software: you can redistribute it and/or modify
|
||||
//it under the terms of the GNU Affero General Public License as published by
|
||||
//the Free Software Foundation, either version 3 of the License, or
|
||||
//(at your option) any later version.
|
||||
|
||||
//This program is distributed in the hope that it will be useful,
|
||||
//but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
//MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
//GNU Affero General Public License for more details.
|
||||
|
||||
//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/>.
|
||||
|
||||
package functions
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestOnlyWhitespace(t *testing.T) {
|
||||
type args struct {
|
||||
word string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want bool
|
||||
}{
|
||||
{"empty string", args{""}, true},
|
||||
{"whitespace", args{" "}, true},
|
||||
{"whitespaces", args{" "}, true},
|
||||
{"whitespaces and tabs", args{" \t"}, true},
|
||||
{"whitespaces and tabs and newlines", args{" \t\n"}, true},
|
||||
{"whitespaces and tabs and newlines and non-breaking spaces", args{" \t\n\u00A0"}, true},
|
||||
{"non-whitespace", args{"a"}, false},
|
||||
{"non-whitespaces", args{"abc"}, false},
|
||||
{"non-whitespaces and tabs", args{"abc\t"}, false},
|
||||
{"non-whitespaces and tabs and newlines", args{"abc\t\n"}, false},
|
||||
{"non-whitespaces and tabs and newlines and non-breaking spaces", args{"abc\t\n\u00A0"}, false},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := OnlyWhitespace(tt.args.word); got != tt.want {
|
||||
t.Errorf("OnlyWhitespace() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHashString(t *testing.T) {
|
||||
type args struct {
|
||||
s string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want string
|
||||
}{
|
||||
{"empty string", args{""}, "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"},
|
||||
{"non-empty string", args{"abc"}, "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := HashString(tt.args.s); got != tt.want {
|
||||
t.Errorf("HashString() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsSeparator(t *testing.T) {
|
||||
type args struct {
|
||||
separator []rune
|
||||
character rune
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want bool
|
||||
}{
|
||||
{"empty separator", args{[]rune{}, 'a'}, false},
|
||||
{"separator with one rune equal", args{[]rune{'a'}, 'a'}, true},
|
||||
{"separator with one rune different", args{[]rune{'a'}, 'b'}, false},
|
||||
{"separator with two runes equal", args{[]rune{'a', 'b'}, 'a'}, true},
|
||||
{"separator with two runes equal second", args{[]rune{'a', 'b'}, 'b'}, true},
|
||||
{"separator with two runes different", args{[]rune{'a', 'b'}, 'c'}, false},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := IsSeparator(tt.args.separator)(tt.args.character); got != tt.want {
|
||||
t.Errorf("IsSeparator()() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestContains(t *testing.T) {
|
||||
type args struct {
|
||||
s []string
|
||||
e string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want bool
|
||||
}{
|
||||
{"empty slice", args{[]string{}, "a"}, false},
|
||||
{"slice with one element equal", args{[]string{"a"}, "a"}, true},
|
||||
{"slice with one element different", args{[]string{"a"}, "b"}, false},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := Contains(tt.args.s, tt.args.e); got != tt.want {
|
||||
t.Errorf("Contains() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSeperateRoomString(t *testing.T) {
|
||||
type args struct {
|
||||
rooms string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want []string
|
||||
}{
|
||||
{"empty string", args{""}, []string{}},
|
||||
{"one room", args{"a"}, []string{"a"}},
|
||||
{"two rooms", args{"a,b"}, []string{"a", "b"}},
|
||||
{"two rooms with whitespace", args{"a, b"}, []string{"a", "b"}},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := SeperateRoomString(tt.args.rooms); !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("SeperateRoomString() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
26
services/data-manager/service/functions/time/mockClock.go
Normal file
26
services/data-manager/service/functions/time/mockClock.go
Normal file
@@ -0,0 +1,26 @@
|
||||
//Calendar implementation for the HTWK Leipzig timetable. Evaluation and display of the individual dates in iCal format.
|
||||
//Copyright (C) 2024 HTWKalender support@htwkalender.de
|
||||
|
||||
//This program is free software: you can redistribute it and/or modify
|
||||
//it under the terms of the GNU Affero General Public License as published by
|
||||
//the Free Software Foundation, either version 3 of the License, or
|
||||
//(at your option) any later version.
|
||||
|
||||
//This program is distributed in the hope that it will be useful,
|
||||
//but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
//MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
//GNU Affero General Public License for more details.
|
||||
|
||||
//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/>.
|
||||
|
||||
package time
|
||||
|
||||
import "time"
|
||||
|
||||
type MockClock struct {
|
||||
NowTime time.Time
|
||||
}
|
||||
|
||||
func (m MockClock) Now() time.Time { return m.NowTime }
|
||||
func (MockClock) After(d time.Duration) <-chan time.Time { return time.After(d) }
|
36
services/data-manager/service/functions/time/parse.go
Normal file
36
services/data-manager/service/functions/time/parse.go
Normal file
@@ -0,0 +1,36 @@
|
||||
//Calendar implementation for the HTWK Leipzig timetable. Evaluation and display of the individual dates in iCal format.
|
||||
//Copyright (C) 2024 HTWKalender support@htwkalender.de
|
||||
|
||||
//This program is free software: you can redistribute it and/or modify
|
||||
//it under the terms of the GNU Affero General Public License as published by
|
||||
//the Free Software Foundation, either version 3 of the License, or
|
||||
//(at your option) any later version.
|
||||
|
||||
//This program is distributed in the hope that it will be useful,
|
||||
//but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
//MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
//GNU Affero General Public License for more details.
|
||||
|
||||
//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/>.
|
||||
|
||||
package time
|
||||
|
||||
import (
|
||||
"github.com/pocketbase/pocketbase/tools/types"
|
||||
"log/slog"
|
||||
"time"
|
||||
)
|
||||
|
||||
func ParseTime(timeString string) (time.Time, error) {
|
||||
return time.Parse("2006-01-02T15:04:05Z", timeString)
|
||||
}
|
||||
|
||||
func ParseAsTypesDatetime(time time.Time) types.DateTime {
|
||||
dateTime, err := types.ParseDateTime(time)
|
||||
if err != nil {
|
||||
slog.Error("Failed to parse time as types.DateTime", "error", err)
|
||||
return types.DateTime{}
|
||||
}
|
||||
return dateTime
|
||||
}
|
24
services/data-manager/service/functions/time/realClock.go
Normal file
24
services/data-manager/service/functions/time/realClock.go
Normal file
@@ -0,0 +1,24 @@
|
||||
//Calendar implementation for the HTWK Leipzig timetable. Evaluation and display of the individual dates in iCal format.
|
||||
//Copyright (C) 2024 HTWKalender support@htwkalender.de
|
||||
|
||||
//This program is free software: you can redistribute it and/or modify
|
||||
//it under the terms of the GNU Affero General Public License as published by
|
||||
//the Free Software Foundation, either version 3 of the License, or
|
||||
//(at your option) any later version.
|
||||
|
||||
//This program is distributed in the hope that it will be useful,
|
||||
//but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
//MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
//GNU Affero General Public License for more details.
|
||||
|
||||
//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/>.
|
||||
|
||||
package time
|
||||
|
||||
import "time"
|
||||
|
||||
type RealClock struct{}
|
||||
|
||||
func (RealClock) Now() time.Time { return time.Now() }
|
||||
func (RealClock) After(d time.Duration) <-chan time.Time { return time.After(d) }
|
24
services/data-manager/service/functions/time/time.go
Normal file
24
services/data-manager/service/functions/time/time.go
Normal file
@@ -0,0 +1,24 @@
|
||||
//Calendar implementation for the HTWK Leipzig timetable. Evaluation and display of the individual dates in iCal format.
|
||||
//Copyright (C) 2024 HTWKalender support@htwkalender.de
|
||||
|
||||
//This program is free software: you can redistribute it and/or modify
|
||||
//it under the terms of the GNU Affero General Public License as published by
|
||||
//the Free Software Foundation, either version 3 of the License, or
|
||||
//(at your option) any later version.
|
||||
|
||||
//This program is distributed in the hope that it will be useful,
|
||||
//but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
//MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
//GNU Affero General Public License for more details.
|
||||
|
||||
//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/>.
|
||||
|
||||
package time
|
||||
|
||||
import "time"
|
||||
|
||||
type Clock interface {
|
||||
Now() time.Time
|
||||
After(d time.Duration) <-chan time.Time
|
||||
}
|
50
services/data-manager/service/ical/ical.go
Normal file
50
services/data-manager/service/ical/ical.go
Normal file
@@ -0,0 +1,50 @@
|
||||
//Calendar implementation for the HTWK Leipzig timetable. Evaluation and display of the individual dates in iCal format.
|
||||
//Copyright (C) 2024 HTWKalender support@htwkalender.de
|
||||
|
||||
//This program is free software: you can redistribute it and/or modify
|
||||
//it under the terms of the GNU Affero General Public License as published by
|
||||
//the Free Software Foundation, either version 3 of the License, or
|
||||
//(at your option) any later version.
|
||||
|
||||
//This program is distributed in the hope that it will be useful,
|
||||
//but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
//MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
//GNU Affero General Public License for more details.
|
||||
|
||||
//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/>.
|
||||
|
||||
package ical
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"github.com/pocketbase/pocketbase"
|
||||
"github.com/pocketbase/pocketbase/apis"
|
||||
"htwkalender/model"
|
||||
"htwkalender/service/db"
|
||||
)
|
||||
|
||||
func CreateIndividualFeed(requestBody []byte, app *pocketbase.PocketBase) (string, error) {
|
||||
var modules []model.FeedCollection
|
||||
|
||||
err := json.Unmarshal(requestBody, &modules)
|
||||
if err != nil {
|
||||
return "", apis.NewNotFoundError("Could not parse request body", err)
|
||||
}
|
||||
|
||||
var icalFeed model.Feed
|
||||
jsonModules, _ := json.Marshal(modules)
|
||||
icalFeed.Modules = string(jsonModules)
|
||||
|
||||
collection, dbError := db.FindCollection(app, "feeds")
|
||||
if dbError != nil {
|
||||
return "", apis.NewNotFoundError("Collection could not be found", dbError)
|
||||
}
|
||||
|
||||
record, err := db.SaveFeed(icalFeed, collection, app)
|
||||
if err != nil {
|
||||
return "", apis.NewNotFoundError("Could not save feed", err)
|
||||
}
|
||||
|
||||
return record.Id, nil
|
||||
}
|
173
services/data-manager/service/ical/icalFileGeneration.go
Normal file
173
services/data-manager/service/ical/icalFileGeneration.go
Normal file
@@ -0,0 +1,173 @@
|
||||
//Calendar implementation for the HTWK Leipzig timetable. Evaluation and display of the individual dates in iCal format.
|
||||
//Copyright (C) 2024 HTWKalender support@htwkalender.de
|
||||
|
||||
//This program is free software: you can redistribute it and/or modify
|
||||
//it under the terms of the GNU Affero General Public License as published by
|
||||
//the Free Software Foundation, either version 3 of the License, or
|
||||
//(at your option) any later version.
|
||||
|
||||
//This program is distributed in the hope that it will be useful,
|
||||
//but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
//MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
//GNU Affero General Public License for more details.
|
||||
|
||||
//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/>.
|
||||
|
||||
package ical
|
||||
|
||||
import (
|
||||
"htwkalender/model"
|
||||
"htwkalender/service/functions"
|
||||
clock "htwkalender/service/functions/time"
|
||||
"htwkalender/service/names"
|
||||
"time"
|
||||
|
||||
"github.com/jordic/goics"
|
||||
_ "time/tzdata"
|
||||
)
|
||||
|
||||
// IcalModel local type for EmitICal function
|
||||
type IcalModel struct {
|
||||
Events model.Events
|
||||
Mapping map[string]model.FeedCollection
|
||||
}
|
||||
|
||||
// EmitICal implements the interface for goics
|
||||
func (icalModel IcalModel) EmitICal() goics.Componenter {
|
||||
internalClock := clock.RealClock{}
|
||||
c := generateIcalEmit(icalModel, internalClock)
|
||||
return c
|
||||
}
|
||||
|
||||
func generateIcalEmit(icalModel IcalModel, internalClock clock.Clock) *goics.Component {
|
||||
europeTime, _ := time.LoadLocation("Europe/Berlin")
|
||||
c := goics.NewComponent()
|
||||
c.SetType("VCALENDAR")
|
||||
// PRODID is required by the standard
|
||||
c.AddProperty("PRODID", "-//HTWK Kalender//htwkalender.de//DE")
|
||||
|
||||
c.AddProperty("VERSION", "2.0")
|
||||
c.AddProperty("CALSCALE", "GREGORIAN")
|
||||
c.AddProperty("TZID", "EUROPE/BERLIN")
|
||||
c.AddProperty("X-WR-CALNAME", "HTWK Kalender")
|
||||
c.AddProperty("X-WR-TIMEZONE", "EUROPE/BERLIN")
|
||||
//add v time zone
|
||||
icalModel.vtimezone(c)
|
||||
|
||||
for _, event := range icalModel.Events {
|
||||
mapEntry, mappingFound := icalModel.Mapping[event.UUID]
|
||||
|
||||
s := goics.NewComponent()
|
||||
s.SetType("VEVENT")
|
||||
|
||||
s.AddProperty(goics.FormatDateTime("DTSTAMP", internalClock.Now().Local().In(europeTime)))
|
||||
|
||||
// create a unique id for the event by hashing the event start, end, course and name
|
||||
var eventHash = functions.HashString(event.Start.String() + event.End.String() + event.Course + event.Name + event.Rooms)
|
||||
|
||||
s.AddProperty("UID", eventHash+"@htwkalender.de")
|
||||
s.AddProperty(goics.FormatDateTime("DTEND", event.End.Time().Local().In(europeTime)))
|
||||
s.AddProperty(goics.FormatDateTime("DTSTART", event.Start.Time().Local().In(europeTime)))
|
||||
|
||||
if mappingFound {
|
||||
addPropertyIfNotEmpty(s, "SUMMARY", replaceNameIfUserDefined(&event, mapEntry))
|
||||
addAlarmIfSpecified(s, event, mapEntry, internalClock)
|
||||
} else {
|
||||
addPropertyIfNotEmpty(s, "SUMMARY", event.Name)
|
||||
}
|
||||
|
||||
addPropertyIfNotEmpty(s, "DESCRIPTION", generateDescription(event))
|
||||
addPropertyIfNotEmpty(s, "LOCATION", event.Rooms)
|
||||
c.AddComponent(s)
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
func (icalModel IcalModel) vtimezone(c *goics.Component) {
|
||||
tz := goics.NewComponent()
|
||||
tz.SetType("VTIMEZONE")
|
||||
tz.AddProperty("TZID", "EUROPE/BERLIN")
|
||||
//add standard time
|
||||
icalModel.standard(tz)
|
||||
//add daylight time
|
||||
icalModel.daylight(tz)
|
||||
|
||||
c.AddComponent(tz)
|
||||
}
|
||||
|
||||
func (icalModel IcalModel) standard(tz *goics.Component) {
|
||||
st := NewHtwkalenderComponent()
|
||||
st.SetType("STANDARD")
|
||||
st.AddProperty("DTSTART", "19701025T030000")
|
||||
st.AddProperty("RRULE", "FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU")
|
||||
st.AddProperty("TZOFFSETFROM", "+0200")
|
||||
st.AddProperty("TZOFFSETTO", "+0100")
|
||||
st.AddProperty("TZNAME", "CET")
|
||||
tz.AddComponent(st)
|
||||
}
|
||||
|
||||
// create an override for goics component function Write
|
||||
// to add the RRULE property
|
||||
|
||||
func (icalModel IcalModel) daylight(tz *goics.Component) {
|
||||
dt := NewHtwkalenderComponent()
|
||||
dt.SetType("DAYLIGHT")
|
||||
dt.AddProperty("DTSTART", "19700329T020000")
|
||||
dt.AddProperty("TZOFFSETFROM", "+0100")
|
||||
dt.AddProperty("TZOFFSETTO", "+0200")
|
||||
dt.AddProperty("TZNAME", "CEST")
|
||||
dt.AddProperty("RRULE", "FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU")
|
||||
tz.AddComponent(dt)
|
||||
}
|
||||
|
||||
// if reminder is specified in the configuration for this event, an alarm will be added to the event
|
||||
func addAlarmIfSpecified(s *goics.Component, event model.Event, mapping model.FeedCollection, clock clock.Clock) {
|
||||
// if event.Start > now
|
||||
// then add alarm
|
||||
if event.Start.Time().Local().After(clock.Now().Local()) && mapping.Reminder {
|
||||
a := goics.NewComponent()
|
||||
a.SetType("VALARM")
|
||||
a.AddProperty("TRIGGER", "-P0DT0H15M0S")
|
||||
a.AddProperty("ACTION", "DISPLAY")
|
||||
a.AddProperty("DESCRIPTION", "Next course: "+replaceNameIfUserDefined(&event, mapping)+" in "+event.Rooms)
|
||||
s.AddComponent(a)
|
||||
}
|
||||
}
|
||||
|
||||
// replaceNameIfUserDefined replaces the name of the event with the user defined name if it is not empty
|
||||
// all contained template strings will be replaced with the corresponding values from the event
|
||||
func replaceNameIfUserDefined(event *model.Event, mapping model.FeedCollection) string {
|
||||
if !functions.OnlyWhitespace(mapping.UserDefinedName) {
|
||||
return names.ReplaceTemplateSubStrings(mapping.UserDefinedName, *event)
|
||||
}
|
||||
|
||||
return event.Name
|
||||
}
|
||||
|
||||
// AddPropertyIfNotEmpty adds a property to the component if the value is not empty
|
||||
// or contains only whitespaces
|
||||
func addPropertyIfNotEmpty(component *goics.Component, key string, value string) {
|
||||
if !functions.OnlyWhitespace(value) {
|
||||
component.AddProperty(key, value)
|
||||
}
|
||||
}
|
||||
|
||||
func generateDescription(event model.Event) string {
|
||||
var description string
|
||||
|
||||
if !functions.OnlyWhitespace(event.Prof) {
|
||||
description += "Profs: " + event.Prof + "\n"
|
||||
}
|
||||
if !functions.OnlyWhitespace(event.Course) {
|
||||
description += "Gruppen: " + event.Course + "\n"
|
||||
}
|
||||
if !functions.OnlyWhitespace(event.EventType) {
|
||||
description += "Typ: " + event.EventType + event.Compulsory + "\n"
|
||||
}
|
||||
if !functions.OnlyWhitespace(event.Notes) {
|
||||
description += "Notizen: " + event.Notes + "\n"
|
||||
}
|
||||
|
||||
return description
|
||||
}
|
258
services/data-manager/service/ical/icalFileGeneration_test.go
Normal file
258
services/data-manager/service/ical/icalFileGeneration_test.go
Normal file
@@ -0,0 +1,258 @@
|
||||
//Calendar implementation for the HTWK Leipzig timetable. Evaluation and display of the individual dates in iCal format.
|
||||
//Copyright (C) 2024 HTWKalender support@htwkalender.de
|
||||
|
||||
//This program is free software: you can redistribute it and/or modify
|
||||
//it under the terms of the GNU Affero General Public License as published by
|
||||
//the Free Software Foundation, either version 3 of the License, or
|
||||
//(at your option) any later version.
|
||||
|
||||
//This program is distributed in the hope that it will be useful,
|
||||
//but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
//MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
//GNU Affero General Public License for more details.
|
||||
|
||||
//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/>.
|
||||
|
||||
package ical
|
||||
|
||||
import (
|
||||
"github.com/jordic/goics"
|
||||
"htwkalender/model"
|
||||
mockTime "htwkalender/service/functions/time"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestIcalModel_EmitICal(t *testing.T) {
|
||||
type fields struct {
|
||||
Events model.Events
|
||||
Mapping map[string]model.FeedCollection
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
want *goics.Component
|
||||
}{
|
||||
{
|
||||
name: "Test EmitICal",
|
||||
fields: fields{
|
||||
Events: model.Events{
|
||||
{
|
||||
UUID: "123",
|
||||
Name: "Test",
|
||||
EventType: "Test",
|
||||
Notes: "Test",
|
||||
Prof: "Test",
|
||||
Rooms: "Test",
|
||||
BookedAt: "Test",
|
||||
},
|
||||
},
|
||||
Mapping: map[string]model.FeedCollection{
|
||||
"123": {
|
||||
UUID: "123",
|
||||
Name: "Test",
|
||||
Course: "Test",
|
||||
UserDefinedName: "Test",
|
||||
},
|
||||
},
|
||||
},
|
||||
want: &goics.Component{
|
||||
Tipo: "VCALENDAR",
|
||||
Elements: []goics.Componenter{
|
||||
&goics.Component{
|
||||
Tipo: "VTIMEZONE",
|
||||
Elements: []goics.Componenter{
|
||||
&HtwkalenderComponent{
|
||||
Component: &goics.Component{
|
||||
Tipo: "STANDARD",
|
||||
Elements: []goics.Componenter{},
|
||||
Properties: map[string][]string{
|
||||
"DTSTART": {"19701025T030000"},
|
||||
"RRULE": {"FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU"},
|
||||
"TZOFFSETFROM": {"+0200"},
|
||||
"TZOFFSETTO": {"+0100"},
|
||||
"TZNAME": {"CET"},
|
||||
},
|
||||
},
|
||||
},
|
||||
&HtwkalenderComponent{
|
||||
Component: &goics.Component{
|
||||
Tipo: "DAYLIGHT",
|
||||
Elements: []goics.Componenter{},
|
||||
Properties: map[string][]string{
|
||||
"DTSTART": {"19700329T020000"},
|
||||
"TZOFFSETFROM": {"+0100"},
|
||||
"TZOFFSETTO": {"+0200"},
|
||||
"TZNAME": {"CEST"},
|
||||
"RRULE": {"FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Properties: map[string][]string{
|
||||
"TZID": {"EUROPE/BERLIN"},
|
||||
},
|
||||
},
|
||||
&goics.Component{
|
||||
Tipo: "VEVENT",
|
||||
Elements: []goics.Componenter{},
|
||||
Properties: map[string][]string{
|
||||
"DTSTAMP": {"20231201T000000Z"},
|
||||
"UID": {"a8d627d93f518e9096b6f40e36d27b7660fa26d318ef1adc43da750e49ebe4be@htwkalender.de"},
|
||||
"DTEND": {"00010101T000000Z"},
|
||||
"DTSTART": {"00010101T000000Z"},
|
||||
"SUMMARY": {"Test"},
|
||||
"DESCRIPTION": {"Profs: Test\nTyp: Test\nNotizen: Test\n"},
|
||||
"LOCATION": {"Test"},
|
||||
},
|
||||
},
|
||||
},
|
||||
Properties: map[string][]string{
|
||||
"PRODID": {"-//HTWK Kalender//htwkalender.de//DE"},
|
||||
"VERSION": {"2.0"},
|
||||
"CALSCALE": {"GREGORIAN"},
|
||||
"TZID": {"EUROPE/BERLIN"},
|
||||
"X-WR-CALNAME": {"HTWK Kalender"},
|
||||
"X-WR-TIMEZONE": {"EUROPE/BERLIN"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Test Similar Events like Sport Courses",
|
||||
fields: fields{
|
||||
Events: model.Events{
|
||||
{
|
||||
UUID: "123",
|
||||
Name: "Test",
|
||||
Course: "Test",
|
||||
EventType: "Test",
|
||||
Notes: "Test",
|
||||
Prof: "Test",
|
||||
Rooms: "ZU430",
|
||||
BookedAt: "Test",
|
||||
Start: mockTime.ParseAsTypesDatetime(time.Date(2023, 12, 1, 0, 0, 0, 0, time.UTC)),
|
||||
End: mockTime.ParseAsTypesDatetime(time.Date(2023, 12, 1, 1, 0, 0, 0, time.UTC)),
|
||||
},
|
||||
{
|
||||
UUID: "123",
|
||||
Name: "Test",
|
||||
Course: "Test",
|
||||
EventType: "Test",
|
||||
Notes: "Test",
|
||||
Prof: "Test",
|
||||
Rooms: "ZU221",
|
||||
BookedAt: "Test",
|
||||
Start: mockTime.ParseAsTypesDatetime(time.Date(2023, 12, 1, 0, 0, 0, 0, time.UTC)),
|
||||
End: mockTime.ParseAsTypesDatetime(time.Date(2023, 12, 1, 1, 0, 0, 0, time.UTC)),
|
||||
},
|
||||
},
|
||||
Mapping: map[string]model.FeedCollection{
|
||||
"123": {
|
||||
UUID: "123",
|
||||
Name: "Test",
|
||||
Course: "Test",
|
||||
UserDefinedName: "UserDefinedName",
|
||||
},
|
||||
},
|
||||
},
|
||||
want: &goics.Component{
|
||||
Tipo: "VCALENDAR",
|
||||
Elements: []goics.Componenter{
|
||||
&goics.Component{
|
||||
Tipo: "VTIMEZONE",
|
||||
Elements: []goics.Componenter{
|
||||
&HtwkalenderComponent{
|
||||
Component: &goics.Component{
|
||||
Tipo: "STANDARD",
|
||||
Elements: []goics.Componenter{},
|
||||
Properties: map[string][]string{
|
||||
"DTSTART": {"19701025T030000"},
|
||||
"RRULE": {"FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU"},
|
||||
"TZOFFSETFROM": {"+0200"},
|
||||
"TZOFFSETTO": {"+0100"},
|
||||
"TZNAME": {"CET"},
|
||||
},
|
||||
},
|
||||
},
|
||||
&HtwkalenderComponent{
|
||||
Component: &goics.Component{
|
||||
Tipo: "DAYLIGHT",
|
||||
Elements: []goics.Componenter{},
|
||||
Properties: map[string][]string{
|
||||
"DTSTART": {"19700329T020000"},
|
||||
"TZOFFSETFROM": {"+0100"},
|
||||
"TZOFFSETTO": {"+0200"},
|
||||
"TZNAME": {"CEST"},
|
||||
"RRULE": {"FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Properties: map[string][]string{
|
||||
"TZID": {"EUROPE/BERLIN"},
|
||||
},
|
||||
},
|
||||
&goics.Component{
|
||||
Tipo: "VEVENT",
|
||||
Elements: []goics.Componenter{},
|
||||
Properties: map[string][]string{
|
||||
"DTSTAMP": {"20231201T000000Z"},
|
||||
"UID": {"b52a7a081f46eeba9b402114493278a34a48b572c84e53d7ac4da9dea15cdff2@htwkalender.de"},
|
||||
"DTEND": {"20231201T010000Z"},
|
||||
"DTSTART": {"20231201T000000Z"},
|
||||
"SUMMARY": {"UserDefinedName"},
|
||||
"DESCRIPTION": {"Profs: Test\nGruppen: Test\nTyp: Test\nNotizen: Test\n"},
|
||||
"LOCATION": {"ZU430"},
|
||||
},
|
||||
},
|
||||
&goics.Component{
|
||||
Tipo: "VEVENT",
|
||||
Elements: []goics.Componenter{},
|
||||
Properties: map[string][]string{
|
||||
"DTSTAMP": {"20231201T000000Z"},
|
||||
"UID": {"5e946c0c4474bc6e6337262093e3ef31477e026bbc6bab398d755b002506d9d7@htwkalender.de"},
|
||||
"DTEND": {"20231201T010000Z"},
|
||||
"DTSTART": {"20231201T000000Z"},
|
||||
"SUMMARY": {"UserDefinedName"},
|
||||
"DESCRIPTION": {"Profs: Test\nGruppen: Test\nTyp: Test\nNotizen: Test\n"},
|
||||
"LOCATION": {"ZU221"},
|
||||
},
|
||||
},
|
||||
},
|
||||
Properties: map[string][]string{
|
||||
"PRODID": {"-//HTWK Kalender//htwkalender.de//DE"},
|
||||
"VERSION": {"2.0"},
|
||||
"CALSCALE": {"GREGORIAN"},
|
||||
"TZID": {"EUROPE/BERLIN"},
|
||||
"X-WR-CALNAME": {"HTWK Kalender"},
|
||||
"X-WR-TIMEZONE": {"EUROPE/BERLIN"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
icalModel := IcalModel{
|
||||
Events: tt.fields.Events,
|
||||
Mapping: tt.fields.Mapping,
|
||||
}
|
||||
|
||||
mockClock := mockTime.MockClock{
|
||||
NowTime: time.Date(2023, 12, 1, 0, 0, 0, 0, time.UTC),
|
||||
}
|
||||
|
||||
if got := generateIcalEmit(icalModel, mockClock); !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("EmitICal() = \n%v, want \n%v", got, tt.want)
|
||||
|
||||
// Print the differences
|
||||
for i, element := range got.Elements {
|
||||
if !reflect.DeepEqual(element, tt.want.Elements[i]) {
|
||||
t.Errorf("Element %d: got \n%v, want \n%v", i, element, tt.want.Elements[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
62
services/data-manager/service/ical/icsComponenter.go
Normal file
62
services/data-manager/service/ical/icsComponenter.go
Normal file
@@ -0,0 +1,62 @@
|
||||
//Calendar implementation for the HTWK Leipzig timetable. Evaluation and display of the individual dates in iCal format.
|
||||
//Copyright (C) 2024 HTWKalender support@htwkalender.de
|
||||
|
||||
//This program is free software: you can redistribute it and/or modify
|
||||
//it under the terms of the GNU Affero General Public License as published by
|
||||
//the Free Software Foundation, either version 3 of the License, or
|
||||
//(at your option) any later version.
|
||||
|
||||
//This program is distributed in the hope that it will be useful,
|
||||
//but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
//MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
//GNU Affero General Public License for more details.
|
||||
|
||||
//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/>.
|
||||
|
||||
package ical
|
||||
|
||||
import (
|
||||
"github.com/jordic/goics"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type HtwkalenderComponent struct {
|
||||
*goics.Component
|
||||
}
|
||||
|
||||
func NewHtwkalenderComponent() *HtwkalenderComponent {
|
||||
return &HtwkalenderComponent{
|
||||
Component: goics.NewComponent(),
|
||||
}
|
||||
}
|
||||
|
||||
// Writes the component to the Writer
|
||||
func (c *HtwkalenderComponent) Write(w *goics.ICalEncode) {
|
||||
w.WriteLine("BEGIN:" + c.Tipo + goics.CRLF)
|
||||
|
||||
// Iterate over component properties
|
||||
var keys []string
|
||||
for k := range c.Properties {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
for _, key := range keys {
|
||||
vals := c.Properties[key]
|
||||
for _, val := range vals {
|
||||
w.WriteLine(WriteStringField(key, val))
|
||||
}
|
||||
}
|
||||
|
||||
for _, xc := range c.Elements {
|
||||
xc.Write(w)
|
||||
}
|
||||
|
||||
w.WriteLine("END:" + c.Tipo + goics.CRLF)
|
||||
}
|
||||
|
||||
// WriteStringField UID:asdfasdfаs@dfasdf.com
|
||||
func WriteStringField(key string, val string) string {
|
||||
return strings.ToUpper(key) + ":" + (val) + goics.CRLF
|
||||
}
|
@@ -0,0 +1,39 @@
|
||||
//Calendar implementation for the HTWK Leipzig timetable. Evaluation and display of the individual dates in iCal format.
|
||||
//Copyright (C) 2024 HTWKalender support@htwkalender.de
|
||||
|
||||
//This program is free software: you can redistribute it and/or modify
|
||||
//it under the terms of the GNU Affero General Public License as published by
|
||||
//the Free Software Foundation, either version 3 of the License, or
|
||||
//(at your option) any later version.
|
||||
|
||||
//This program is distributed in the hope that it will be useful,
|
||||
//but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
//MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
//GNU Affero General Public License for more details.
|
||||
|
||||
//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/>.
|
||||
|
||||
package names
|
||||
|
||||
import (
|
||||
"htwkalender/model"
|
||||
"regexp"
|
||||
)
|
||||
|
||||
func ReplaceTemplateSubStrings(rawString string, event model.Event) string {
|
||||
re := regexp.MustCompile(`\%(.)`)
|
||||
|
||||
return re.ReplaceAllStringFunc(rawString, func(match string) string {
|
||||
switch match {
|
||||
case "%%":
|
||||
return "%"
|
||||
case "%t":
|
||||
return event.EventType
|
||||
case "%p":
|
||||
return event.Compulsory
|
||||
default:
|
||||
return match
|
||||
}
|
||||
})
|
||||
}
|
@@ -0,0 +1,94 @@
|
||||
//Calendar implementation for the HTWK Leipzig timetable. Evaluation and display of the individual dates in iCal format.
|
||||
//Copyright (C) 2024 HTWKalender support@htwkalender.de
|
||||
|
||||
//This program is free software: you can redistribute it and/or modify
|
||||
//it under the terms of the GNU Affero General Public License as published by
|
||||
//the Free Software Foundation, either version 3 of the License, or
|
||||
//(at your option) any later version.
|
||||
|
||||
//This program is distributed in the hope that it will be useful,
|
||||
//but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
//MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
//GNU Affero General Public License for more details.
|
||||
|
||||
//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/>.
|
||||
|
||||
package names
|
||||
|
||||
import (
|
||||
"htwkalender/model"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestReplaceTemplateSubStrings(t *testing.T) {
|
||||
type args struct {
|
||||
rawString string
|
||||
event model.Event
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "Test 1",
|
||||
args: args{
|
||||
rawString: "%t",
|
||||
event: model.Event{
|
||||
EventType: "Test",
|
||||
},
|
||||
},
|
||||
want: "Test",
|
||||
},
|
||||
{
|
||||
name: "Test 2",
|
||||
args: args{
|
||||
rawString: "%p",
|
||||
event: model.Event{
|
||||
Compulsory: "Test",
|
||||
},
|
||||
},
|
||||
want: "Test",
|
||||
},
|
||||
{
|
||||
name: "Test 3",
|
||||
args: args{
|
||||
rawString: "%%",
|
||||
event: model.Event{
|
||||
EventType: "Test",
|
||||
},
|
||||
},
|
||||
want: "%",
|
||||
},
|
||||
{
|
||||
name: "Test 4",
|
||||
args: args{
|
||||
rawString: "%t %p",
|
||||
event: model.Event{
|
||||
EventType: "Test",
|
||||
Compulsory: "Test",
|
||||
},
|
||||
},
|
||||
want: "Test Test",
|
||||
},
|
||||
{
|
||||
name: "Test 5",
|
||||
args: args{
|
||||
rawString: "%t %p %%",
|
||||
event: model.Event{
|
||||
EventType: "Test",
|
||||
Compulsory: "Test",
|
||||
},
|
||||
},
|
||||
want: "Test Test %",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := ReplaceTemplateSubStrings(tt.args.rawString, tt.args.event); got != tt.want {
|
||||
t.Errorf("ReplaceTemplateSubStrings() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
106
services/data-manager/service/room/roomService.go
Normal file
106
services/data-manager/service/room/roomService.go
Normal file
@@ -0,0 +1,106 @@
|
||||
//Calendar implementation for the HTWK Leipzig timetable. Evaluation and display of the individual dates in iCal format.
|
||||
//Copyright (C) 2024 HTWKalender support@htwkalender.de
|
||||
|
||||
//This program is free software: you can redistribute it and/or modify
|
||||
//it under the terms of the GNU Affero General Public License as published by
|
||||
//the Free Software Foundation, either version 3 of the License, or
|
||||
//(at your option) any later version.
|
||||
|
||||
//This program is distributed in the hope that it will be useful,
|
||||
//but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
//MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
//GNU Affero General Public License for more details.
|
||||
|
||||
//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/>.
|
||||
|
||||
package room
|
||||
|
||||
import (
|
||||
"github.com/pocketbase/pocketbase"
|
||||
"htwkalender/model"
|
||||
"htwkalender/service/db"
|
||||
"htwkalender/service/functions"
|
||||
"time"
|
||||
)
|
||||
|
||||
func GetRooms(app *pocketbase.PocketBase) ([]string, error) {
|
||||
rooms, err := db.GetRooms(app)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
return rooms, nil
|
||||
}
|
||||
}
|
||||
|
||||
func GetRoomScheduleForDay(app *pocketbase.PocketBase, room string, date string) ([]model.AnonymizedEventDTO, error) {
|
||||
roomSchedule, err := db.GetRoomScheduleForDay(app, room, date)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
anonymizedRoomSchedule := anonymizeRooms(roomSchedule)
|
||||
return anonymizedRoomSchedule, nil
|
||||
}
|
||||
|
||||
func GetRoomSchedule(app *pocketbase.PocketBase, room string, from string, to string) ([]model.AnonymizedEventDTO, error) {
|
||||
roomSchedule, err := db.GetRoomSchedule(app, room, from, to)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
anonymizedRoomSchedule := anonymizeRooms(roomSchedule)
|
||||
return anonymizedRoomSchedule, nil
|
||||
}
|
||||
|
||||
// Transform the events to anonymized events throwing away all unnecessary information
|
||||
func anonymizeRooms(events []model.Event) []model.AnonymizedEventDTO {
|
||||
var anonymizedEvents []model.AnonymizedEventDTO
|
||||
for _, event := range events {
|
||||
anonymizedEvents = append(anonymizedEvents, event.AnonymizeEvent())
|
||||
}
|
||||
return anonymizedEvents
|
||||
}
|
||||
|
||||
func GetFreeRooms(app *pocketbase.PocketBase, from time.Time, to time.Time) ([]string, error) {
|
||||
rooms, err := db.GetRooms(app)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var events model.Events
|
||||
events, err = db.GetEventsThatCollideWithTimeRange(app, from, to)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
freeRooms := removeRoomsThatHaveEvents(rooms, events)
|
||||
return freeRooms, nil
|
||||
}
|
||||
|
||||
// Remove all rooms from the list that have events in the given time range
|
||||
func removeRoomsThatHaveEvents(rooms []string, schedule []model.Event) []string {
|
||||
var freeRooms []string
|
||||
for _, room := range rooms {
|
||||
if !isRoomInSchedule(room, schedule) {
|
||||
freeRooms = append(freeRooms, room)
|
||||
}
|
||||
}
|
||||
return freeRooms
|
||||
}
|
||||
|
||||
// Check if a room is in the schedule
|
||||
func isRoomInSchedule(room string, schedule []model.Event) bool {
|
||||
for _, event := range schedule {
|
||||
if event.Course != "Sport" {
|
||||
rooms := functions.SeperateRoomString(event.Rooms)
|
||||
// check if room is in rooms
|
||||
for _, r := range rooms {
|
||||
if r == room {
|
||||
return true
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if event.Rooms == room {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
351
services/data-manager/service/room/roomService_test.go
Normal file
351
services/data-manager/service/room/roomService_test.go
Normal file
@@ -0,0 +1,351 @@
|
||||
//Calendar implementation for the HTWK Leipzig timetable. Evaluation and display of the individual dates in iCal format.
|
||||
//Copyright (C) 2024 HTWKalender support@htwkalender.de
|
||||
|
||||
//This program is free software: you can redistribute it and/or modify
|
||||
//it under the terms of the GNU Affero General Public License as published by
|
||||
//the Free Software Foundation, either version 3 of the License, or
|
||||
//(at your option) any later version.
|
||||
|
||||
//This program is distributed in the hope that it will be useful,
|
||||
//but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
//MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
//GNU Affero General Public License for more details.
|
||||
|
||||
//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/>.
|
||||
|
||||
package room
|
||||
|
||||
import (
|
||||
"github.com/pocketbase/pocketbase/tools/types"
|
||||
"htwkalender/model"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func Test_anonymizeRooms(t *testing.T) {
|
||||
type args struct {
|
||||
events []model.Event
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want []model.AnonymizedEventDTO
|
||||
}{
|
||||
{
|
||||
name: "anonymize single event",
|
||||
args: args{
|
||||
events: []model.Event{
|
||||
{
|
||||
UUID: "testUUID",
|
||||
Day: "Montag",
|
||||
Week: "52",
|
||||
Start: types.DateTime{},
|
||||
End: types.DateTime{},
|
||||
Name: "Secret",
|
||||
EventType: "V",
|
||||
Prof: "Prof. Dr. Secret",
|
||||
Rooms: "Room",
|
||||
Notes: "Secret",
|
||||
BookedAt: "Secret",
|
||||
Course: "42INM-3",
|
||||
Semester: "ws",
|
||||
Compulsory: "p",
|
||||
},
|
||||
},
|
||||
},
|
||||
want: []model.AnonymizedEventDTO{
|
||||
{
|
||||
Day: "Montag",
|
||||
Week: "52",
|
||||
Start: types.DateTime{},
|
||||
End: types.DateTime{},
|
||||
Rooms: "Room",
|
||||
Free: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "anonymize empty list",
|
||||
args: args{
|
||||
events: []model.Event{},
|
||||
},
|
||||
want: nil,
|
||||
},
|
||||
{
|
||||
name: "anonymize multiple events",
|
||||
args: args{
|
||||
events: []model.Event{
|
||||
{
|
||||
UUID: "testUUID1",
|
||||
Day: "Montag",
|
||||
Week: "51",
|
||||
Start: types.DateTime{},
|
||||
End: types.DateTime{},
|
||||
Name: "Incognito",
|
||||
EventType: "V",
|
||||
Prof: "Prof. Dr. Incognito",
|
||||
Rooms: "Room",
|
||||
Notes: "Incognito",
|
||||
BookedAt: "Incognito",
|
||||
Course: "69INM-2",
|
||||
Semester: "sose",
|
||||
Compulsory: "p",
|
||||
},
|
||||
{
|
||||
UUID: "testUUID2",
|
||||
Day: "Dienstag",
|
||||
Week: "52",
|
||||
Start: types.DateTime{},
|
||||
End: types.DateTime{},
|
||||
Name: "Private",
|
||||
EventType: "S",
|
||||
Prof: "Prof.In. Dr.-Ing. Private",
|
||||
Rooms: "Room",
|
||||
Notes: "Private",
|
||||
BookedAt: "Private",
|
||||
Course: "42MIM-3",
|
||||
Semester: "ws",
|
||||
Compulsory: "w",
|
||||
},
|
||||
},
|
||||
},
|
||||
want: []model.AnonymizedEventDTO{
|
||||
{
|
||||
Day: "Montag",
|
||||
Week: "51",
|
||||
Start: types.DateTime{},
|
||||
End: types.DateTime{},
|
||||
Rooms: "Room",
|
||||
Free: false,
|
||||
},
|
||||
{
|
||||
Day: "Dienstag",
|
||||
Week: "52",
|
||||
Start: types.DateTime{},
|
||||
End: types.DateTime{},
|
||||
Rooms: "Room",
|
||||
Free: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := anonymizeRooms(tt.args.events); !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("anonymizeRooms() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_isRoomInSchedule(t *testing.T) {
|
||||
type args struct {
|
||||
room string
|
||||
schedule []model.Event
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "room is in schedule",
|
||||
args: args{
|
||||
room: "Room",
|
||||
schedule: []model.Event{
|
||||
{
|
||||
UUID: "testUUID",
|
||||
Day: "Montag",
|
||||
Week: "52",
|
||||
Start: types.DateTime{},
|
||||
End: types.DateTime{},
|
||||
Name: "Secret",
|
||||
EventType: "V",
|
||||
Prof: "Prof. Dr. Secret",
|
||||
Rooms: "Room",
|
||||
Notes: "Secret",
|
||||
},
|
||||
},
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "room is not in schedule",
|
||||
args: args{
|
||||
room: "Z324",
|
||||
schedule: []model.Event{
|
||||
{
|
||||
UUID: "testUUID",
|
||||
Day: "Montag",
|
||||
Week: "52",
|
||||
Start: types.DateTime{},
|
||||
End: types.DateTime{},
|
||||
Name: "Secret",
|
||||
EventType: "V",
|
||||
Prof: "Prof. Dr. Bond",
|
||||
Rooms: "LI007",
|
||||
Notes: "Keine Zeit für die Uni",
|
||||
},
|
||||
},
|
||||
},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "schedule event.Course is sport",
|
||||
args: args{
|
||||
room: "Klettergerüst",
|
||||
schedule: []model.Event{
|
||||
{
|
||||
UUID: "903784265784639527",
|
||||
Day: "Montag",
|
||||
Week: "52",
|
||||
Start: types.DateTime{},
|
||||
End: types.DateTime{},
|
||||
Name: "Hampelmann",
|
||||
EventType: "S",
|
||||
Prof: "Prof. Dr. Bewegung",
|
||||
Rooms: "Klettergerüst",
|
||||
Notes: "A apple a day keeps the doctor away",
|
||||
Course: "Sport",
|
||||
},
|
||||
},
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "schedule event.Course is sport with different room",
|
||||
args: args{
|
||||
room: "HTWK Sportplatz",
|
||||
schedule: []model.Event{
|
||||
{
|
||||
UUID: "903784265784639527",
|
||||
Day: "Montag",
|
||||
Week: "52",
|
||||
Start: types.DateTime{},
|
||||
End: types.DateTime{},
|
||||
Name: "Hampelmann",
|
||||
EventType: "S",
|
||||
Prof: "Prof. Dr. Bewegung",
|
||||
Rooms: "Klettergerüst",
|
||||
Notes: "A apple a day keeps the doctor away",
|
||||
Course: "Sport",
|
||||
},
|
||||
},
|
||||
},
|
||||
want: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := isRoomInSchedule(tt.args.room, tt.args.schedule); got != tt.want {
|
||||
t.Errorf("isRoomInSchedule() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_getFreeRooms(t *testing.T) {
|
||||
type args struct {
|
||||
rooms []string
|
||||
schedule []model.Event
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want []string
|
||||
}{
|
||||
{
|
||||
name: "remove room1 from list",
|
||||
args: args{
|
||||
rooms: []string{
|
||||
"Room1",
|
||||
"Room2",
|
||||
"Room3",
|
||||
},
|
||||
schedule: []model.Event{
|
||||
{
|
||||
UUID: "testUUID",
|
||||
Day: "Montag",
|
||||
Week: "52",
|
||||
Start: types.DateTime{},
|
||||
End: types.DateTime{},
|
||||
Name: "Secret",
|
||||
EventType: "V",
|
||||
Prof: "Prof. Dr. Secret",
|
||||
Rooms: "Room1",
|
||||
Notes: "Secret",
|
||||
},
|
||||
},
|
||||
},
|
||||
want: []string{
|
||||
"Room2",
|
||||
"Room3",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "remove room2 from list",
|
||||
args: args{
|
||||
rooms: []string{
|
||||
"Room1",
|
||||
"Room2",
|
||||
"Room3",
|
||||
},
|
||||
schedule: []model.Event{
|
||||
{
|
||||
UUID: "testUUID",
|
||||
Day: "Montag",
|
||||
Week: "52",
|
||||
Start: types.DateTime{},
|
||||
End: types.DateTime{},
|
||||
Name: "Secret",
|
||||
EventType: "V",
|
||||
Prof: "Prof. Dr. Secret",
|
||||
Rooms: "Room3",
|
||||
Notes: "Secret",
|
||||
},
|
||||
},
|
||||
},
|
||||
want: []string{
|
||||
"Room1",
|
||||
"Room2",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "remove no room from list",
|
||||
args: args{
|
||||
rooms: []string{
|
||||
"Room1",
|
||||
"Room2",
|
||||
"Room3",
|
||||
},
|
||||
schedule: []model.Event{
|
||||
{
|
||||
UUID: "testUUID",
|
||||
Day: "Montag",
|
||||
Week: "52",
|
||||
Start: types.DateTime{},
|
||||
End: types.DateTime{},
|
||||
Name: "Secret",
|
||||
EventType: "V",
|
||||
Prof: "Prof. Dr. Secret",
|
||||
Rooms: "Room4",
|
||||
Notes: "Secret",
|
||||
},
|
||||
},
|
||||
},
|
||||
want: []string{
|
||||
"Room1",
|
||||
"Room2",
|
||||
"Room3",
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := removeRoomsThatHaveEvents(tt.args.rooms, tt.args.schedule); !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("removeRoomsThatHaveEvents() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user