Merge branch 'development' into 'main'

Update Main

See merge request htwk-software/htwkalender!5
This commit is contained in:
Elmar Kresse
2024-05-19 23:02:24 +00:00
32 changed files with 1043 additions and 377 deletions

View File

@@ -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)
}
}

View File

@@ -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,

View File

@@ -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)
}
})
}
}

View File

@@ -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)
})
}
}

View File

@@ -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)
})
}
}

View File

@@ -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{

View File

@@ -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")
}

View File

@@ -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)
}
}
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View 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)
}
})
}
}

View File

@@ -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")
}

View File

@@ -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)

View File

@@ -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,
})
}
}

View File

@@ -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)
}
})

View File

@@ -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
}

View File

@@ -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)

View File

@@ -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)
}
})
}
}

View File

@@ -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"}
}

View 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)
}
})
}
}

View File

@@ -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))

View File

@@ -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)
}
})
}
}

View File

@@ -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

View File

@@ -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 <https://www.gnu.org/licenses/>.
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
}

View File

@@ -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" {

View File

@@ -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) {

View File

@@ -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

View 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/>.
// function to fetch course data from the API
export async function fetchEventTypes(): Promise<string[]> {
const eventTypes: string[] = [];
await fetch("/api/events/types")
.then((response) => {
return response.json() as Promise<string[]>;
})
.then((responseModules: string[]) => {
responseModules.forEach((eventType: string) => {
eventTypes.push(
eventType,
);
});
});
return eventTypes;
}

View File

@@ -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<string[]> = ref([]);
const mobilePage = inject("mobilePage") as Ref<boolean>;
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<Module[]> = 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) {
</Column>
<Column
field="eventType"
filter-field="eventType"
:filter-menu-style="{ width: '10rem' }"
style="min-width: 10rem"
:header="$t('additionalModules.eventType')"
:show-clear-button="false"
:show-filter-menu="false"
>
<template #filter="{ filterModel, filterCallback }">
<InputText
<MultiSelect
v-model="filterModel.value"
type="text"
:options="eventTypes"
class="p-column-filter max-w-10rem"
@input="filterCallback()"
style="min-width: 10rem"
@change="filterCallback()"
/>
</template>
<template v-if="loadingData" #body>

View File

@@ -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;