diff --git a/backend/main.go b/backend/main.go
index d39757a..8b78ba1 100644
--- a/backend/main.go
+++ b/backend/main.go
@@ -41,6 +41,6 @@ func main() {
service.AddSchedules(app)
if err := app.Start(); err != nil {
- slog.Error("Failed to start app: %v", err)
+ slog.Error("Failed to start app: ", "error", err)
}
}
diff --git a/backend/model/eventModel.go b/backend/model/eventModel.go
index 142fa0b..2debe6d 100644
--- a/backend/model/eventModel.go
+++ b/backend/model/eventModel.go
@@ -57,6 +57,10 @@ type Event struct {
models.BaseModel
}
+type EventType struct {
+ EventType string `db:"EventType" json:"eventType"`
+}
+
func (e *Event) Equals(event Event) bool {
return e.Day == event.Day &&
e.Week == event.Week &&
@@ -79,7 +83,7 @@ func (e *Event) SetCourse(course string) Event {
return *e
}
-// Creates an AnonymizedEventDTO from an Event hiding all sensitive data
+// AnonymizeEvent Creates an AnonymizedEventDTO from an Event hiding all sensitive data
func (e *Event) AnonymizeEvent() AnonymizedEventDTO {
return AnonymizedEventDTO{
Day: e.Day,
diff --git a/backend/model/eventModel_test.go b/backend/model/eventModel_test.go
index 95d4954..c153f23 100644
--- a/backend/model/eventModel_test.go
+++ b/backend/model/eventModel_test.go
@@ -217,3 +217,264 @@ func TestEvent_AnonymizeEvent(t *testing.T) {
})
}
}
+
+func TestEvent_GetName(t *testing.T) {
+ type fields struct {
+ UUID string
+ Day string
+ Week string
+ Start types.DateTime
+ End types.DateTime
+ Name string
+ EventType string
+ Compulsory string
+ Prof string
+ Rooms string
+ Notes string
+ BookedAt string
+ Course string
+ Semester string
+ BaseModel models.BaseModel
+ }
+ tests := []struct {
+ name string
+ fields fields
+ want string
+ }{
+ {
+ name: "empty event",
+ fields: fields{},
+ want: "",
+ },
+ {
+ name: "one event",
+ fields: fields{Name: "Event"},
+ want: "Event",
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ e := &Event{
+ UUID: tt.fields.UUID,
+ Day: tt.fields.Day,
+ Week: tt.fields.Week,
+ Start: tt.fields.Start,
+ End: tt.fields.End,
+ Name: tt.fields.Name,
+ EventType: tt.fields.EventType,
+ Compulsory: tt.fields.Compulsory,
+ Prof: tt.fields.Prof,
+ Rooms: tt.fields.Rooms,
+ Notes: tt.fields.Notes,
+ BookedAt: tt.fields.BookedAt,
+ Course: tt.fields.Course,
+ Semester: tt.fields.Semester,
+ BaseModel: tt.fields.BaseModel,
+ }
+ if got := e.GetName(); got != tt.want {
+ t.Errorf("GetName() = %v, want %v", got, tt.want)
+ }
+ })
+ }
+}
+
+func TestEvent_SetCourse(t *testing.T) {
+ type fields struct {
+ UUID string
+ Day string
+ Week string
+ Start types.DateTime
+ End types.DateTime
+ Name string
+ EventType string
+ Compulsory string
+ Prof string
+ Rooms string
+ Notes string
+ BookedAt string
+ Course string
+ Semester string
+ BaseModel models.BaseModel
+ }
+ type args struct {
+ course string
+ }
+ tests := []struct {
+ name string
+ fields fields
+ args args
+ want Event
+ }{
+ {
+ name: "set course",
+ fields: fields{},
+ args: args{course: "test"},
+ want: Event{Course: "test"},
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ e := &Event{
+ UUID: tt.fields.UUID,
+ Day: tt.fields.Day,
+ Week: tt.fields.Week,
+ Start: tt.fields.Start,
+ End: tt.fields.End,
+ Name: tt.fields.Name,
+ EventType: tt.fields.EventType,
+ Compulsory: tt.fields.Compulsory,
+ Prof: tt.fields.Prof,
+ Rooms: tt.fields.Rooms,
+ Notes: tt.fields.Notes,
+ BookedAt: tt.fields.BookedAt,
+ Course: tt.fields.Course,
+ Semester: tt.fields.Semester,
+ BaseModel: tt.fields.BaseModel,
+ }
+ if got := e.SetCourse(tt.args.course); !reflect.DeepEqual(got, tt.want) {
+ t.Errorf("SetCourse() = %v, want %v", got, tt.want)
+ }
+ })
+ }
+}
+
+func TestEvent_SetName(t *testing.T) {
+ type fields struct {
+ UUID string
+ Day string
+ Week string
+ Start types.DateTime
+ End types.DateTime
+ Name string
+ EventType string
+ Compulsory string
+ Prof string
+ Rooms string
+ Notes string
+ BookedAt string
+ Course string
+ Semester string
+ BaseModel models.BaseModel
+ }
+ type args struct {
+ name string
+ }
+ tests := []struct {
+ name string
+ fields fields
+ args args
+ }{
+ {
+ name: "set name",
+ fields: fields{
+ Name: "name",
+ },
+ args: args{
+ name: "name",
+ },
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ e := &Event{
+ UUID: tt.fields.UUID,
+ Day: tt.fields.Day,
+ Week: tt.fields.Week,
+ Start: tt.fields.Start,
+ End: tt.fields.End,
+ Name: tt.fields.Name,
+ EventType: tt.fields.EventType,
+ Compulsory: tt.fields.Compulsory,
+ Prof: tt.fields.Prof,
+ Rooms: tt.fields.Rooms,
+ Notes: tt.fields.Notes,
+ BookedAt: tt.fields.BookedAt,
+ Course: tt.fields.Course,
+ Semester: tt.fields.Semester,
+ BaseModel: tt.fields.BaseModel,
+ }
+ e.SetName(tt.args.name)
+ })
+ }
+}
+
+func TestEvent_TableName(t *testing.T) {
+ type fields struct {
+ UUID string
+ Day string
+ Week string
+ Start types.DateTime
+ End types.DateTime
+ Name string
+ EventType string
+ Compulsory string
+ Prof string
+ Rooms string
+ Notes string
+ BookedAt string
+ Course string
+ Semester string
+ BaseModel models.BaseModel
+ }
+ tests := []struct {
+ name string
+ fields fields
+ want string
+ }{
+ {
+ name: "table name",
+ fields: fields{},
+ want: "events",
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ e := &Event{
+ UUID: tt.fields.UUID,
+ Day: tt.fields.Day,
+ Week: tt.fields.Week,
+ Start: tt.fields.Start,
+ End: tt.fields.End,
+ Name: tt.fields.Name,
+ EventType: tt.fields.EventType,
+ Compulsory: tt.fields.Compulsory,
+ Prof: tt.fields.Prof,
+ Rooms: tt.fields.Rooms,
+ Notes: tt.fields.Notes,
+ BookedAt: tt.fields.BookedAt,
+ Course: tt.fields.Course,
+ Semester: tt.fields.Semester,
+ BaseModel: tt.fields.BaseModel,
+ }
+ if got := e.TableName(); got != tt.want {
+ t.Errorf("TableName() = %v, want %v", got, tt.want)
+ }
+ })
+ }
+}
+
+func TestEvents_Contains1(t *testing.T) {
+ type args struct {
+ event Event
+ }
+ tests := []struct {
+ name string
+ m Events
+ args args
+ want bool
+ }{
+ {
+ name: "empty events",
+ m: Events{},
+ args: args{event: Event{}},
+ want: false,
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ if got := tt.m.Contains(tt.args.event); got != tt.want {
+ t.Errorf("Contains() = %v, want %v", got, tt.want)
+ }
+ })
+ }
+}
diff --git a/backend/model/feedModel_test.go b/backend/model/feedModel_test.go
new file mode 100644
index 0000000..111d8d1
--- /dev/null
+++ b/backend/model/feedModel_test.go
@@ -0,0 +1,45 @@
+package model
+
+import (
+ "github.com/pocketbase/pocketbase/models"
+ "github.com/pocketbase/pocketbase/tools/types"
+ "testing"
+)
+
+func TestFeed_SetModules(t *testing.T) {
+ type fields struct {
+ Modules string
+ Retrieved types.DateTime
+ BaseModel models.BaseModel
+ }
+ type args struct {
+ modules string
+ }
+ tests := []struct {
+ name string
+ fields fields
+ args args
+ }{
+ {
+ name: "set modules",
+ fields: fields{
+ Modules: "",
+ Retrieved: types.DateTime{},
+ BaseModel: models.BaseModel{},
+ },
+ args: args{
+ modules: "modules",
+ },
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ f := &Feed{
+ Modules: tt.fields.Modules,
+ Retrieved: tt.fields.Retrieved,
+ BaseModel: tt.fields.BaseModel,
+ }
+ f.SetModules(tt.args.modules)
+ })
+ }
+}
diff --git a/backend/model/moduleModel_test.go b/backend/model/moduleModel_test.go
new file mode 100644
index 0000000..b2ebe5d
--- /dev/null
+++ b/backend/model/moduleModel_test.go
@@ -0,0 +1,126 @@
+package model
+
+import "testing"
+
+func TestModuleDTO_GetName(t *testing.T) {
+ type fields struct {
+ UUID string
+ Name string
+ Prof string
+ Course string
+ Semester string
+ EventType string
+ }
+ tests := []struct {
+ name string
+ fields fields
+ want string
+ }{
+ {
+ name: "get name",
+ fields: fields{
+ Name: "name",
+ },
+ want: "name",
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ m := &ModuleDTO{
+ UUID: tt.fields.UUID,
+ Name: tt.fields.Name,
+ Prof: tt.fields.Prof,
+ Course: tt.fields.Course,
+ Semester: tt.fields.Semester,
+ EventType: tt.fields.EventType,
+ }
+ if got := m.GetName(); got != tt.want {
+ t.Errorf("GetName() = %v, want %v", got, tt.want)
+ }
+ })
+ }
+}
+
+func TestModuleDTO_SetName(t *testing.T) {
+ type fields struct {
+ UUID string
+ Name string
+ Prof string
+ Course string
+ Semester string
+ EventType string
+ }
+ type args struct {
+ name string
+ }
+ tests := []struct {
+ name string
+ fields fields
+ args args
+ }{
+ {
+ name: "set name",
+ fields: fields{
+ Name: "name",
+ },
+ args: args{
+ name: "name",
+ },
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ m := &ModuleDTO{
+ UUID: tt.fields.UUID,
+ Name: tt.fields.Name,
+ Prof: tt.fields.Prof,
+ Course: tt.fields.Course,
+ Semester: tt.fields.Semester,
+ EventType: tt.fields.EventType,
+ }
+ m.SetName(tt.args.name)
+ })
+ }
+}
+
+func TestModule_SetName(t *testing.T) {
+ type fields struct {
+ UUID string
+ Name string
+ Prof string
+ Course string
+ Semester string
+ Events Events
+ }
+ type args struct {
+ name string
+ }
+ tests := []struct {
+ name string
+ fields fields
+ args args
+ }{
+ {
+ name: "set name",
+ fields: fields{
+ Name: "name",
+ },
+ args: args{
+ name: "name",
+ },
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ m := &Module{
+ UUID: tt.fields.UUID,
+ Name: tt.fields.Name,
+ Prof: tt.fields.Prof,
+ Course: tt.fields.Course,
+ Semester: tt.fields.Semester,
+ Events: tt.fields.Events,
+ }
+ m.SetName(tt.args.name)
+ })
+ }
+}
diff --git a/backend/service/addRoute.go b/backend/service/addRoute.go
index 9938aca..0f131bf 100644
--- a/backend/service/addRoute.go
+++ b/backend/service/addRoute.go
@@ -17,12 +17,12 @@
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/ical"
"htwkalender/service/room"
"log/slog"
"net/http"
@@ -42,7 +42,7 @@ func AddRoutes(app *pocketbase.PocketBase) {
Handler: func(c echo.Context) error {
savedEvents, err := v2.ParseEventsFromRemote(app)
if err != nil {
- slog.Error("Failed to parse events from remote: %v", err)
+ 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)
@@ -59,6 +59,25 @@ func AddRoutes(app *pocketbase.PocketBase) {
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,
@@ -157,7 +176,7 @@ func AddRoutes(app *pocketbase.PocketBase) {
date := c.QueryParam("date")
roomSchedule, err := room.GetRoomScheduleForDay(app, roomParam, date)
if err != nil {
- slog.Error("Failed to get room schedule for day: %v", err)
+ 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)
@@ -183,7 +202,7 @@ func AddRoutes(app *pocketbase.PocketBase) {
from := c.QueryParam("from")
roomSchedule, err := room.GetRoomSchedule(app, roomParam, from, to)
if err != nil {
- slog.Error("Failed to get room schedule: %v", err)
+ 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)
@@ -206,17 +225,17 @@ func AddRoutes(app *pocketbase.PocketBase) {
Handler: func(c echo.Context) error {
from, err := time.ParseTime(c.QueryParam("from"))
if err != nil {
- slog.Error("Failed to parse time: %v", err)
+ 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: %v", err)
+ 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: %v", err)
+ 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)
@@ -238,12 +257,14 @@ func AddRoutes(app *pocketbase.PocketBase) {
Method: http.MethodGet,
Path: "/api/course/modules",
Handler: func(c echo.Context) error {
- course := c.QueryParam("course")
- semester := c.QueryParam("semester")
- modules, err := events.GetModulesForCourseDistinct(app, course, semester)
+ modules, err := events.GetModulesForCourseDistinct(
+ app,
+ c.QueryParam("course"),
+ c.QueryParam("semester"),
+ )
if err != nil {
- slog.Error("Failed to get modules for course and semester: %v", err)
+ 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)
@@ -266,7 +287,7 @@ func AddRoutes(app *pocketbase.PocketBase) {
Handler: func(c echo.Context) error {
modules, err := events.GetAllModulesDistinct(app)
if err != nil {
- slog.Error("Failed to get modules: %v", err)
+ slog.Error("Failed to get modules: ", "error", err)
return c.JSON(http.StatusBadRequest, "Failed to get modules")
}
return c.JSON(http.StatusOK, modules)
@@ -289,7 +310,7 @@ func AddRoutes(app *pocketbase.PocketBase) {
requestModule := c.QueryParam("uuid")
module, err := events.GetModuleByUUID(app, requestModule)
if err != nil {
- slog.Error("Failed to get module: %v", err)
+ slog.Error("Failed to get module: ", "error", err)
return c.JSON(http.StatusBadRequest, "Failed to get module")
} else {
return c.JSON(http.StatusOK, module)
@@ -339,10 +360,34 @@ func AddRoutes(app *pocketbase.PocketBase) {
courses, err := events.GetAllCoursesForSemesterWithEvents(app, semester)
if err != nil {
- slog.Error("Failed to get courses for semester with events: %v", err)
+ 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(200, courses)
+ 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{
@@ -360,39 +405,16 @@ func AddRoutes(app *pocketbase.PocketBase) {
Method: http.MethodDelete,
Path: "/api/events",
Handler: func(c echo.Context) error {
- course := c.QueryParam("course")
- semester := c.QueryParam("semester")
- err := events.DeleteAllEventsByCourseAndSemester(app, course, semester)
+ err := events.DeleteAllEventsByCourseAndSemester(
+ app,
+ c.QueryParam("course"),
+ c.QueryParam("semester"),
+ )
if err != nil {
- slog.Error("Failed to delete events: %v", err)
+ slog.Error("Failed to delete events: ", "error", err)
return c.JSON(http.StatusBadRequest, "Failed to delete events")
} else {
- return c.JSON(http.StatusBadRequest, "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/feeds/migrate",
- Handler: func(c echo.Context) error {
- err := ical.MigrateFeedJson(app)
-
- if err != nil {
- slog.Error("Failed to migrate feeds: %v", err)
- return c.JSON(http.StatusInternalServerError, "Failed to migrate feeds")
- } else {
- return c.JSON(http.StatusOK, "Migrated")
+ return c.JSON(http.StatusOK, "Events deleted")
}
},
Middlewares: []echo.MiddlewareFunc{
diff --git a/backend/service/addSchedule.go b/backend/service/addSchedule.go
index 444429e..9d2b901 100644
--- a/backend/service/addSchedule.go
+++ b/backend/service/addSchedule.go
@@ -23,6 +23,7 @@ import (
"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"
@@ -34,10 +35,21 @@ func AddSchedules(app *pocketbase.PocketBase) {
app.OnBeforeServe().Add(func(e *core.ServeEvent) error {
scheduler := cron.New()
- // Every hour update all courses (5 segments - minute, hour, day, month, weekday) "0 * * * *"
- // Every three hours update all courses (5 segments - minute, hour, day, month, weekday) "0 */3 * * *"
- // Every 10 minutes update all courses (5 segments - minute, hour, day, month, weekday) "*/10 * * * *"
- scheduler.MustAdd("updateCourse", "0 */3 * * *", func() {
+ // !! 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)
})
@@ -54,16 +66,16 @@ func AddSchedules(app *pocketbase.PocketBase) {
slog.Info("Started fetching sport events schedule")
sportEvents, err := sport.FetchAndUpdateSportEvents(app)
if err != nil {
- slog.Error("Failed to fetch and save sport events: %v", err)
+ 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 2 * * 0", func() {
+ 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: %v", err)
+ slog.Error("Failed to fetch and save events: ", "error", err)
} else {
slog.Info("Successfully fetched " + strconv.FormatInt(int64(len(savedEvents)), 10) + " events")
}
diff --git a/backend/service/course/courseFunctions.go b/backend/service/course/courseFunctions.go
index 43720b7..d51ad00 100644
--- a/backend/service/course/courseFunctions.go
+++ b/backend/service/course/courseFunctions.go
@@ -20,17 +20,14 @@ import (
"github.com/pocketbase/pocketbase"
"htwkalender/service/events"
"log/slog"
- "strconv"
)
func UpdateCourse(app *pocketbase.PocketBase) {
courses := events.GetAllCourses(app)
for _, course := range courses {
- savedEvents, err := events.UpdateModulesForCourse(app, course)
+ _, err := events.UpdateModulesForCourse(app, course)
if err != nil {
- slog.Warn("Update Course: "+course+" failed: %v", err)
- } else {
- slog.Info("Updated Course: " + course + " with " + strconv.FormatInt(int64(len(savedEvents)), 10) + " events")
+ slog.Warn("Update Course: "+course+" failed:", "error", err)
}
}
}
diff --git a/backend/service/date/dateFormat.go b/backend/service/date/dateFormat.go
index a8e9c16..8eee644 100644
--- a/backend/service/date/dateFormat.go
+++ b/backend/service/date/dateFormat.go
@@ -29,7 +29,7 @@ func GetDateFromWeekNumber(year int, weekNumber int, dayName string) (time.Time,
europeTime, err := time.LoadLocation("Europe/Berlin")
if err != nil {
- slog.Error("Failed to load location: ", err)
+ slog.Error("Failed to load location: ", "error", err)
return time.Time{}, err
}
diff --git a/backend/service/db/dbEvents.go b/backend/service/db/dbEvents.go
index 5edf53f..8db7b64 100644
--- a/backend/service/db/dbEvents.go
+++ b/backend/service/db/dbEvents.go
@@ -27,24 +27,22 @@ import (
"github.com/pocketbase/pocketbase"
)
-func SaveSeminarGroupEvents(seminarGroups []model.SeminarGroup, app *pocketbase.PocketBase) ([]model.Event, error) {
+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 _, seminarGroup := range seminarGroups {
- for _, event := range seminarGroup.Events {
- event = event.SetCourse(seminarGroup.Course)
- existsInDatabase, err := findEventByDayWeekStartEndNameCourse(event, seminarGroup.Course, app)
- alreadyAddedToSave := toBeSavedEvents.Contains(event)
+ 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 err != nil {
+ return nil, err
+ }
- if !existsInDatabase && !alreadyAddedToSave {
- toBeSavedEvents = append(toBeSavedEvents, event)
- }
+ if !existsInDatabase && !alreadyAddedToSave {
+ toBeSavedEvents = append(toBeSavedEvents, event)
}
}
@@ -194,13 +192,26 @@ func GetPlanForModules(app *pocketbase.PocketBase, modules map[string]model.Feed
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: ", err)
+ 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)
}
@@ -212,7 +223,7 @@ func GetAllModulesDistinctByNameAndCourse(app *pocketbase.PocketBase) ([]model.M
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: ", err)
+ 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")
}
@@ -356,6 +367,17 @@ func GetEventsThatStartBeforeAndEndBefore(app *pocketbase.PocketBase, from types
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)
@@ -366,3 +388,13 @@ func GetEventsThatStartAfterAndEndAfter(app *pocketbase.PocketBase, from types.D
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
+}
diff --git a/backend/service/db/dbGroups.go b/backend/service/db/dbGroups.go
index 1f51c15..1e5d02c 100644
--- a/backend/service/db/dbGroups.go
+++ b/backend/service/db/dbGroups.go
@@ -85,7 +85,7 @@ func GetAllCourses(app *pocketbase.PocketBase) []string {
// 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: ", err)
+ slog.Error("Error while getting groups from database: ", "error", err)
return []string{}
}
@@ -107,7 +107,7 @@ func GetAllCoursesForSemester(app *pocketbase.PocketBase, semester string) []str
// 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: ", err)
+ slog.Error("Error while getting groups from database: ", "error", err)
return []string{}
}
@@ -130,7 +130,7 @@ func GetAllCoursesForSemesterWithEvents(app *pocketbase.PocketBase, semester str
// 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: ", err)
+ slog.Error("Error while getting groups from database: ", "error", err)
return nil, err
}
diff --git a/backend/service/events/eventService.go b/backend/service/events/eventService.go
index 5c84efb..77b8bb1 100644
--- a/backend/service/events/eventService.go
+++ b/backend/service/events/eventService.go
@@ -22,6 +22,8 @@ import (
"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) {
@@ -120,15 +122,11 @@ func DeleteAllEvents(app *pocketbase.PocketBase) error {
// If the update was not successful, an error is returned
func UpdateModulesForCourse(app *pocketbase.PocketBase, course string) (model.Events, error) {
- //new string array with one element (course)
- var courses []string
- courses = append(courses, course)
+ seminarGroup := v1.GetSeminarGroupEventsFromHTML(course)
- seminarGroups := v1.GetSeminarGroupsEventsFromHTML(courses)
+ seminarGroup = v1.ClearEmptySeminarGroups(seminarGroup)
- seminarGroups = v1.ClearEmptySeminarGroups(seminarGroups)
-
- seminarGroups = v1.ReplaceEmptyEventNames(seminarGroups)
+ seminarGroup = v1.ReplaceEmptyEventNames(seminarGroup)
//check if events in the seminarGroups Events are already in the database
//if yes, keep the database as it is
@@ -136,58 +134,53 @@ func UpdateModulesForCourse(app *pocketbase.PocketBase, course string) (model.Ev
//if there are no events in the database, save the new events
//get all events for the course and the semester
- events, err := db.GetAllModulesForCourse(app, course, "ws")
+ dbEvents, err := db.GetAllEventsForCourse(app, course)
if err != nil {
return nil, err
}
- // append all events for the course and the semester to the events array for ss
- summerEvents, err := db.GetAllModulesForCourse(app, course, "ss")
- if err != nil {
- return nil, err
- }
-
- events = append(events, summerEvents...)
-
//if there are no events in the database, save the new events
- if len(events) == 0 {
- events, dbError := db.SaveSeminarGroupEvents(seminarGroups, app)
+ if len(dbEvents) == 0 {
+ events, dbError := db.SaveSeminarGroupEvents(seminarGroup, app)
if dbError != nil {
return nil, dbError
}
return events, nil
}
- //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
+ // Create partial update list and delete list for the events
+ var insertList model.Events
+ var deleteList model.Events
- var savedEvents model.Events
-
- for _, seminarGroup := range seminarGroups {
- for _, event := range seminarGroup.Events {
- // if the event is not in the database, delete all events for the course and the semester and save the new events
- if !ContainsEvent(events, event) {
-
- err = DeleteAllEventsByCourseAndSemester(app, course, "ws")
- if err != nil {
- return nil, err
- }
-
- err = DeleteAllEventsByCourseAndSemester(app, course, "ss")
- if err != nil {
- return nil, err
- }
-
- //save the new events
- savedEvent, dbError := db.SaveSeminarGroupEvents(seminarGroups, app)
- if dbError != nil {
- return nil, dbError
- }
- savedEvents = append(savedEvents, savedEvent...)
- }
+ // 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
}
@@ -205,3 +198,18 @@ func ContainsEvent(events model.Events, event model.Event) bool {
}
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
+}
diff --git a/backend/service/events/eventService_test.go b/backend/service/events/eventService_test.go
new file mode 100644
index 0000000..6c5c23e
--- /dev/null
+++ b/backend/service/events/eventService_test.go
@@ -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)
+ }
+ })
+ }
+}
diff --git a/backend/service/feed/feedFunctions.go b/backend/service/feed/feedFunctions.go
index 050c5e2..894f322 100644
--- a/backend/service/feed/feedFunctions.go
+++ b/backend/service/feed/feedFunctions.go
@@ -30,7 +30,7 @@ import (
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", err)
+ slog.Error("CleanFeeds: failed to get all feeds", "error", err)
return
}
for _, feed := range feeds {
@@ -44,8 +44,8 @@ func ClearFeeds(db *daos.Dao, months int, clock localTime.Clock) {
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", err)
- slog.Error("SQL Result: ", sqlResult)
+ 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")
}
diff --git a/backend/service/fetch/sport/sportFetcher.go b/backend/service/fetch/sport/sportFetcher.go
index f17f41c..c2dac05 100644
--- a/backend/service/fetch/sport/sportFetcher.go
+++ b/backend/service/fetch/sport/sportFetcher.go
@@ -24,6 +24,7 @@ import (
"htwkalender/model"
"htwkalender/service/db"
"htwkalender/service/functions"
+ clock "htwkalender/service/functions/time"
"io"
"log/slog"
"net/http"
@@ -81,7 +82,7 @@ func FetchAndUpdateSportEvents(app *pocketbase.PocketBase) ([]model.Event, error
}
// @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())
+ err = db.DeleteAllEventsByCourse(app, "Sport", functions.GetCurrentSemesterString(clock.RealClock{}))
if err != nil {
return nil, err
}
@@ -208,7 +209,7 @@ func getWeekEvents(start time.Time, end time.Time, cycle string) ([]time.Time, [
for _, day := range days {
weekDay, err := getDayInt(day)
if err != nil {
- slog.Error("Error while getting day int: "+day+" ", err)
+ 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()),
@@ -233,7 +234,8 @@ func getWeekEvents(start time.Time, end time.Time, cycle string) ([]time.Time, [
endI, endIErr = getDayInt(days[1])
if endIErr != nil || startIErr != nil {
- slog.Error("Error while getting day int: "+days[0]+" - "+days[1]+" :", startIErr, endIErr)
+ 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
@@ -258,7 +260,7 @@ func getWeekEvents(start time.Time, end time.Time, cycle string) ([]time.Time, [
dayInt, err := getDayInt(day)
if err != nil {
- slog.Error("Error while getting day int: "+day+" ", err)
+ slog.Error("Error while getting day int: "+day+" ", "error", err)
} else {
dayNumbers = append(dayNumbers, dayInt)
}
@@ -270,7 +272,7 @@ func getWeekEvents(start time.Time, end time.Time, cycle string) ([]time.Time, [
weekDay, err := getDayInt(day)
if err != nil {
- slog.Error("Error while getting day int: "+day+" ", err)
+ 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()),
@@ -377,7 +379,7 @@ func fetchAllAvailableSportCourses() ([]string, error) {
var doc, err = htmlRequest(url)
if err != nil {
- slog.Error("Error while fetching sport courses from webpage", err)
+ slog.Error("Error while fetching sport courses from webpage", "error", err)
return nil, err
}
@@ -442,7 +444,7 @@ func htmlRequest(url string) (*goquery.Document, error) {
defer func(Body io.ReadCloser) {
readErr := Body.Close()
if readErr != nil {
- slog.Error("Error while closing response body from html request", readErr)
+ slog.Error("Error while closing response body from html request", "error", readErr)
return
}
}(resp.Body)
diff --git a/backend/service/fetch/v1/fetchSeminarEventService.go b/backend/service/fetch/v1/fetchSeminarEventService.go
index c592340..ca02906 100644
--- a/backend/service/fetch/v1/fetchSeminarEventService.go
+++ b/backend/service/fetch/v1/fetchSeminarEventService.go
@@ -32,50 +32,43 @@ import (
"time"
)
-func ReplaceEmptyEventNames(groups []model.SeminarGroup) []model.SeminarGroup {
- for i, group := range groups {
- for j, event := range group.Events {
- if functions.OnlyWhitespace(event.Name) {
- groups[i].Events[j].Name = "Sonderveranstaltungen"
- }
+func ReplaceEmptyEventNames(group model.SeminarGroup) model.SeminarGroup {
+ for j, event := range group.Events {
+ if functions.OnlyWhitespace(event.Name) {
+ group.Events[j].Name = "Sonderveranstaltungen"
}
}
- return groups
+ return group
}
-func ClearEmptySeminarGroups(seminarGroups []model.SeminarGroup) []model.SeminarGroup {
- var newSeminarGroups []model.SeminarGroup
- for _, seminarGroup := range seminarGroups {
- if len(seminarGroup.Events) > 0 && seminarGroup.Course != "" {
- newSeminarGroups = append(newSeminarGroups, seminarGroup)
- }
+func ClearEmptySeminarGroups(seminarGroup model.SeminarGroup) model.SeminarGroup {
+ var newSeminarGroup = model.SeminarGroup{}
+
+ if len(seminarGroup.Events) > 0 && seminarGroup.Course != "" {
+ newSeminarGroup = seminarGroup
}
- return newSeminarGroups
+ return newSeminarGroup
}
-func GetSeminarGroupsEventsFromHTML(seminarGroupsLabel []string) []model.SeminarGroup {
- var seminarGroups []model.SeminarGroup
- for _, seminarGroupLabel := range seminarGroupsLabel {
+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)
- seminarGroups = append(seminarGroups, seminarGroup)
- }
- }
-
- 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)
- seminarGroups = append(seminarGroups, 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)
}
}
- return seminarGroups
+
+ 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) {
@@ -110,19 +103,18 @@ func parseSeminarGroup(result string) model.SeminarGroup {
if eventTables == nil || allDayLabels == nil {
return model.SeminarGroup{}
}
-
- eventsWithCombinedWeeks := toEvents(eventTables, allDayLabels)
+ 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
- course := findFirstSpanWithClass(table, "header-2-0-1").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: %s", err)
+ slog.Error("Error occurred while splitting event types:", "error", err)
return model.SeminarGroup{}
}
@@ -211,7 +203,7 @@ func extractSemesterAndYear(semesterString string) (string, string) {
return semesterShortcut, year
}
-func toEvents(tables [][]*html.Node, days []string) []model.Event {
+func toEvents(tables [][]*html.Node, days []string, course string) []model.Event {
var events []model.Event
for table := range tables {
@@ -232,6 +224,7 @@ func toEvents(tables [][]*html.Node, days []string) []model.Event {
Rooms: getTextContent(tableData[6]),
Notes: getTextContent(tableData[7]),
BookedAt: getTextContent(tableData[8]),
+ Course: course,
})
}
}
diff --git a/backend/service/fetch/v1/fetchSeminarEventService_test.go b/backend/service/fetch/v1/fetchSeminarEventService_test.go
index 614660a..a5c103b 100644
--- a/backend/service/fetch/v1/fetchSeminarEventService_test.go
+++ b/backend/service/fetch/v1/fetchSeminarEventService_test.go
@@ -75,28 +75,17 @@ func Test_extractSemesterAndYear(t *testing.T) {
func Test_replaceEmptyEventNames(t *testing.T) {
type args struct {
- groups []model.SeminarGroup
+ group model.SeminarGroup
}
tests := []struct {
name string
args args
- want []model.SeminarGroup
+ want model.SeminarGroup
}{
{
name: "Test 1",
args: args{
- groups: []model.SeminarGroup{
- {
- Events: []model.Event{
- {
- Name: "Test",
- },
- },
- },
- },
- },
- want: []model.SeminarGroup{
- {
+ group: model.SeminarGroup{
Events: []model.Event{
{
Name: "Test",
@@ -104,26 +93,29 @@ func Test_replaceEmptyEventNames(t *testing.T) {
},
},
},
+ want: model.SeminarGroup{
+ Events: []model.Event{
+ {
+ Name: "Test",
+ },
+ },
+ },
},
{
name: "Test 1",
args: args{
- groups: []model.SeminarGroup{
- {
- Events: []model.Event{
- {
- Name: "",
- },
+ group: model.SeminarGroup{
+ Events: []model.Event{
+ {
+ Name: "",
},
},
},
},
- want: []model.SeminarGroup{
- {
- Events: []model.Event{
- {
- Name: "Sonderveranstaltungen",
- },
+ want: model.SeminarGroup{
+ Events: []model.Event{
+ {
+ Name: "Sonderveranstaltungen",
},
},
},
@@ -131,7 +123,7 @@ func Test_replaceEmptyEventNames(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
- if got := ReplaceEmptyEventNames(tt.args.groups); !reflect.DeepEqual(got, tt.want) {
+ if got := ReplaceEmptyEventNames(tt.args.group); !reflect.DeepEqual(got, tt.want) {
t.Errorf("ReplaceEmptyEventNames() = %v, want %v", got, tt.want)
}
})
diff --git a/backend/service/fetch/v1/fetchSeminarGroupService.go b/backend/service/fetch/v1/fetchSeminarGroupService.go
index 45abf67..9e909e5 100644
--- a/backend/service/fetch/v1/fetchSeminarGroupService.go
+++ b/backend/service/fetch/v1/fetchSeminarGroupService.go
@@ -23,6 +23,8 @@ import (
"github.com/pocketbase/pocketbase/models"
"htwkalender/model"
"htwkalender/service/db"
+ "htwkalender/service/functions"
+ "htwkalender/service/functions/time"
"io"
"log/slog"
"net/http"
@@ -59,36 +61,32 @@ func getSeminarHTML(semester string) (string, error) {
func FetchSeminarGroups(app *pocketbase.PocketBase) ([]*models.Record, error) {
var groups []model.SeminarGroup
- resultSummer, err := getSeminarHTML("ss")
+ semesterString := functions.CalculateSemesterList(time.RealClock{})
+ var results [2]string
+ var err error
- if err != nil {
- slog.Error("Error while fetching seminar groups for winter semester", err)
- return nil, err
+ 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)...)
}
- resultWinter, _ := getSeminarHTML("ws")
-
- if err != nil {
- slog.Error("Error while fetching seminar groups for summer semester", err)
- return nil, err
- }
-
- groups = parseSeminarGroups(resultSummer, "ss")
- groups = append(groups, parseSeminarGroups(resultWinter, "ws")...)
-
// filter duplicates
groups = removeDuplicates(groups)
collection, dbError := db.FindCollection(app, "groups")
if dbError != nil {
- slog.Error("Error while searching collection groups", dbError)
+ 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", dbError)
+ slog.Error("Error while saving groups", "error", dbError)
return nil, err
}
diff --git a/backend/service/fetch/v2/fetcher.go b/backend/service/fetch/v2/fetcher.go
index 57a9aef..b26fe8c 100644
--- a/backend/service/fetch/v2/fetcher.go
+++ b/backend/service/fetch/v2/fetcher.go
@@ -25,10 +25,10 @@ import (
"htwkalender/service/db"
"htwkalender/service/fetch"
v1 "htwkalender/service/fetch/v1"
+ "htwkalender/service/functions"
localTime "htwkalender/service/functions/time"
"log/slog"
"strings"
- "time"
)
func ParseEventsFromRemote(app *pocketbase.PocketBase) (model.Events, error) {
@@ -70,7 +70,7 @@ func FetchAllEventsAndSave(app *pocketbase.PocketBase, clock localTime.Clock) ([
}
// Fetch and save events for all semesters
- for _, semester := range calculateSemesterList(clock) {
+ 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)
@@ -104,25 +104,6 @@ func fetchAndSaveAllEventsForSemester(
return savedRecords, err
}
-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", "ws"}
- }
-
- if summerSemester {
- return []string{"ss"}
- }
-
- if winterSemester {
- return []string{"ws"}
- }
-
- return []string{"ss", "ws"}
-}
-
func parseEventForOneSemester(url string) ([]model.Event, error) {
// Fetch Webpage from URL
webpage, err := fetch.GetHTML(url)
@@ -160,7 +141,7 @@ func parseEventForOneSemester(url string) ([]model.Event, error) {
events = convertWeeksToDates(events, semester, year)
events, err = v1.SplitEventType(events)
if err != nil {
- slog.Error("Error occurred while splitting event types: %s", err)
+ slog.Error("Error occurred while splitting event types: ", "error", err)
return nil, err
}
events = switchNameAndNotesForExam(events)
diff --git a/backend/service/fetch/v2/fetcher_test.go b/backend/service/fetch/v2/fetcher_test.go
index 2223549..9b2dec1 100644
--- a/backend/service/fetch/v2/fetcher_test.go
+++ b/backend/service/fetch/v2/fetcher_test.go
@@ -18,10 +18,8 @@ package v2
import (
"htwkalender/model"
- mockTime "htwkalender/service/functions/time"
"reflect"
"testing"
- "time"
)
func Test_switchNameAndNotesForExam(t *testing.T) {
@@ -99,49 +97,3 @@ func Test_switchNameAndNotesForExam(t *testing.T) {
})
}
}
-
-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)
- }
- })
- }
-}
diff --git a/backend/service/functions/semester.go b/backend/service/functions/semester.go
index b2f3b6a..2ac4ea6 100644
--- a/backend/service/functions/semester.go
+++ b/backend/service/functions/semester.go
@@ -16,15 +16,32 @@
package functions
-import "time"
+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() string {
-
- if time.Now().Month() >= 10 || time.Now().Month() <= 3 {
+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"}
+}
diff --git a/backend/service/functions/semester_test.go b/backend/service/functions/semester_test.go
new file mode 100644
index 0000000..eb71898
--- /dev/null
+++ b/backend/service/functions/semester_test.go
@@ -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)
+ }
+ })
+ }
+}
diff --git a/backend/service/functions/string.go b/backend/service/functions/string.go
index a0a74c0..77984a4 100644
--- a/backend/service/functions/string.go
+++ b/backend/service/functions/string.go
@@ -48,13 +48,6 @@ func Contains(s []string, e string) bool {
return false
}
-func ReplaceEmptyString(word string, replacement string) string {
- if OnlyWhitespace(word) {
- return replacement
- }
- return word
-}
-
func HashString(s string) string {
hash := sha256.New()
hash.Write([]byte(s))
diff --git a/backend/service/functions/string_test.go b/backend/service/functions/string_test.go
index 7075f0a..ee8316a 100644
--- a/backend/service/functions/string_test.go
+++ b/backend/service/functions/string_test.go
@@ -17,6 +17,7 @@
package functions
import (
+ "reflect"
"testing"
)
@@ -96,3 +97,49 @@ func TestIsSeparator(t *testing.T) {
})
}
}
+
+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)
+ }
+ })
+ }
+}
diff --git a/backend/service/functions/time/parse.go b/backend/service/functions/time/parse.go
index 9a5fd93..46523bc 100644
--- a/backend/service/functions/time/parse.go
+++ b/backend/service/functions/time/parse.go
@@ -29,7 +29,7 @@ func ParseTime(timeString string) (time.Time, error) {
func ParseAsTypesDatetime(time time.Time) types.DateTime {
dateTime, err := types.ParseDateTime(time)
if err != nil {
- slog.Error("Failed to parse time as types.DateTime: %v", err)
+ slog.Error("Failed to parse time as types.DateTime", "error", err)
return types.DateTime{}
}
return dateTime
diff --git a/backend/service/ical/icalJsonMigrate.go b/backend/service/ical/icalJsonMigrate.go
deleted file mode 100644
index 328f6ae..0000000
--- a/backend/service/ical/icalJsonMigrate.go
+++ /dev/null
@@ -1,74 +0,0 @@
-//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 .
-
-package ical
-
-import (
- "encoding/json"
- "github.com/pocketbase/dbx"
- "github.com/pocketbase/pocketbase"
- "htwkalender/model"
-)
-
-//update ical feed json
-//add uuid field
-//remove module name field
-
-func MigrateFeedJson(app *pocketbase.PocketBase) error {
-
- records, err := app.Dao().FindRecordsByFilter("feeds", "1=1", "-created", 0, 0)
- if err != nil {
- return err
- }
-
- for _, feed := range records {
-
- var modules []model.FeedCollection
-
- err := json.Unmarshal([]byte(feed.GetString("modules")), &modules)
- if err != nil {
- return err
- }
-
- var uuidFeedCollections []model.FeedCollection
-
- for _, module := range modules {
- uuid := searchUUIDForModule(app, module)
-
- if uuid != "" {
- uuidFeedCollections = append(uuidFeedCollections, model.FeedCollection{UUID: uuid, Name: module.Name, Course: module.Course, UserDefinedName: module.UserDefinedName})
- }
- }
-
- jsonModules, _ := json.Marshal(uuidFeedCollections)
- feed.Set("modules", string(jsonModules))
-
- err = app.Dao().SaveRecord(feed)
- if err != nil {
- return err
- }
- }
- return nil
-}
-
-func searchUUIDForModule(app *pocketbase.PocketBase, module model.FeedCollection) string {
- var event model.Event
- err := app.Dao().DB().Select("*").From("events").Where(dbx.NewExp("Name = {:name} AND course = {:course}", dbx.Params{"name": module.Name, "course": module.Course})).One(&event)
- if err != nil {
- return ""
- }
- return event.UUID
-}
diff --git a/backend/service/room/roomService.go b/backend/service/room/roomService.go
index e8a18c5..fdf9ab9 100644
--- a/backend/service/room/roomService.go
+++ b/backend/service/room/roomService.go
@@ -74,6 +74,7 @@ func GetFreeRooms(app *pocketbase.PocketBase, from time.Time, to time.Time) ([]s
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 {
@@ -84,6 +85,7 @@ func removeRoomsThatHaveEvents(rooms []string, schedule []model.Event) []string
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" {
diff --git a/backend/service/room/roomService_test.go b/backend/service/room/roomService_test.go
index 10a6b41..4f0f91f 100644
--- a/backend/service/room/roomService_test.go
+++ b/backend/service/room/roomService_test.go
@@ -191,6 +191,50 @@ func Test_isRoomInSchedule(t *testing.T) {
},
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) {
diff --git a/docker-compose.yml b/docker-compose.yml
index 7c77c23..5b2ce9b 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -23,8 +23,8 @@ services:
context: ./backend
target: dev # prod
command: "--http=0.0.0.0:8090 --dir=/htwkalender/data/pb_data"
- #ports:
- # - "8090:8090"
+ ports:
+ - "8090:8090"
volumes:
- pb_data:/htwkalender/data # for production with volume
# - ./backend:/htwkalender/data # for development with bind mount from project directory
diff --git a/frontend/src/api/fetchEvents.ts b/frontend/src/api/fetchEvents.ts
new file mode 100644
index 0000000..7738036
--- /dev/null
+++ b/frontend/src/api/fetchEvents.ts
@@ -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 .
+
+// function to fetch course data from the API
+
+export async function fetchEventTypes(): Promise {
+ const eventTypes: string[] = [];
+ await fetch("/api/events/types")
+ .then((response) => {
+ return response.json() as Promise;
+ })
+ .then((responseModules: string[]) => {
+ responseModules.forEach((eventType: string) => {
+ eventTypes.push(
+ eventType,
+ );
+ });
+ });
+ return eventTypes;
+}
\ No newline at end of file
diff --git a/frontend/src/components/AdditionalModuleTable.vue b/frontend/src/components/AdditionalModuleTable.vue
index e62e436..d437567 100644
--- a/frontend/src/components/AdditionalModuleTable.vue
+++ b/frontend/src/components/AdditionalModuleTable.vue
@@ -30,6 +30,7 @@ import { useDialog } from "primevue/usedialog";
import router from "../router";
import { fetchModule } from "../api/fetchModule.ts";
import { useI18n } from "vue-i18n";
+import { fetchEventTypes } from "../api/fetchEvents.ts";
const dialog = useDialog();
const { t } = useI18n({ useScope: "global" });
@@ -39,6 +40,9 @@ if (store.isEmpty()) {
router.replace("/");
}
+const eventTypes: Ref = ref([]);
+
+
const mobilePage = inject("mobilePage") as Ref;
const filters = ref({
course: {
@@ -51,7 +55,7 @@ const filters = ref({
},
eventType: {
value: null,
- matchMode: FilterMatchMode.CONTAINS,
+ matchMode: FilterMatchMode.IN,
},
prof: {
value: null,
@@ -63,7 +67,7 @@ const loadedModules: Ref = ref(new Array(10));
const loadingData = ref(true);
-onMounted(() => {
+onMounted( () => {
fetchAllModules()
.then(
(data) =>
@@ -74,6 +78,10 @@ onMounted(() => {
.finally(() => {
loadingData.value = false;
});
+
+ fetchEventTypes().then((data) => {
+ eventTypes.value = data;
+ });
});
const ModuleInformation = defineAsyncComponent(
@@ -184,16 +192,20 @@ function unselectModule(event: DataTableRowUnselectEvent) {
-
diff --git a/reverseproxy.conf b/reverseproxy.conf
index ff64ab2..0a6d63f 100644
--- a/reverseproxy.conf
+++ b/reverseproxy.conf
@@ -196,6 +196,24 @@ http {
limit_req zone=modules burst=5 nodelay;
}
+ location /api/events/types {
+ proxy_pass http://htwkalender-backend:8090;
+ client_max_body_size 20m;
+ proxy_connect_timeout 600s;
+ proxy_read_timeout 600s;
+ proxy_send_timeout 600s;
+ send_timeout 600s;
+ proxy_cache_bypass 0;
+ proxy_no_cache 0;
+ proxy_cache mcache; # mcache=RAM
+ proxy_cache_valid 200 301 302 10m;
+ proxy_cache_valid 403 404 5m;
+ proxy_cache_lock on;
+ proxy_cache_use_stale timeout updating;
+ add_header X-Proxy-Cache $upstream_cache_status;
+ limit_req zone=modules burst=10 nodelay;
+ }
+
location /api/rooms {
proxy_pass http://htwkalender-backend:8090;
client_max_body_size 20m;