//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 db import ( "fmt" "github.com/google/uuid" "github.com/pocketbase/pocketbase/core" "github.com/pocketbase/pocketbase/tools/types" "htwkalender/data-manager/model" "htwkalender/data-manager/service/functions" "log/slog" "time" "github.com/pocketbase/dbx" "github.com/pocketbase/pocketbase" ) var _ core.RecordProxy = (*Event)(nil) type Event struct { core.BaseRecordProxy } type Events []*Event func (event *Event) GetDay() string { return event.GetString("Day") } func (event *Event) SetDay(day string) { event.Set("Day", day) } func (event *Event) GetWeek() string { return event.GetString("Week") } func (event *Event) SetWeek(week string) { event.Set("Week", week) } func (event *Event) GetName() string { return event.GetString("Name") } func (event *Event) SetName(name string) { event.Set("Name", name) } func (event *Event) GetEventType() string { return event.GetString("EventType") } func (event *Event) SetEventType(eventType string) { event.Set("EventType", eventType) } func (event *Event) GetProf() string { return event.GetString("Prof") } func (event *Event) SetProf(prof string) { event.Set("Prof", prof) } func (event *Event) GetRooms() string { return event.GetString("Rooms") } func (event *Event) SetRooms(rooms string) { event.Set("Rooms", rooms) } func (event *Event) GetNotes() string { return event.GetString("Notes") } func (event *Event) SetNotes(notes string) { event.Set("Notes", notes) } func (event *Event) GetBookedAt() string { return event.GetString("BookedAt") } func (event *Event) SetBookedAt(bookedAt string) { event.Set("BookedAt", bookedAt) } func (event *Event) GetCourse() string { return event.GetString("course") } func (event *Event) SetCourse(course string) { event.Set("course", course) } func (event *Event) GetSemester() string { return event.GetString("semester") } func (event *Event) SetSemester(semester string) { event.Set("semester", semester) } func (event *Event) GetUUID() string { return event.GetString("uuid") } func (event *Event) SetUUID(uuid string) { event.Set("uuid", uuid) } func (event *Event) GetStart() types.DateTime { return event.GetDateTime("start") } func (event *Event) SetStart(start types.DateTime) { event.Set("start", start) } func (event *Event) GetEnd() types.DateTime { return event.GetDateTime("end") } func (event *Event) SetEnd(end types.DateTime) { event.Set("end", end) } func (event *Event) GetCompulsory() string { return event.GetString("Compulsory") } func (event *Event) SetCompulsory(compulsory string) { event.Set("Compulsory", compulsory) } func (event *Event) GetCreated() types.DateTime { return event.GetDateTime("created") } func (event *Event) SetCreated(created types.DateTime) { event.Set("created", created) } func (event *Event) GetUpdated() types.DateTime { return event.GetDateTime("updated") } func (event *Event) SetUpdated(updated types.DateTime) { event.Set("updated", updated) } func newEvent(record *core.Record) *Event { return &Event{ BaseRecordProxy: core.BaseRecordProxy{Record: record}, } } func NewEvent(collection *core.Collection, event model.Event) (*Event, error) { // Create a new Event instance if collection.Name != "events" { return nil, core.ErrInvalidFieldValue } record := core.NewRecord(collection) record.Set("id:autogenerate", "") ev := newEvent(record) // Set the fields from the model ev.SetDay(event.Day) ev.SetWeek(event.Week) ev.SetStart(event.Start) ev.SetEnd(event.End) ev.SetName(event.Name) ev.SetEventType(event.EventType) ev.SetProf(event.Prof) ev.SetRooms(event.Rooms) ev.SetNotes(event.Notes) ev.SetBookedAt(event.BookedAt) ev.SetCourse(event.Course) ev.SetSemester(event.Semester) ev.SetCompulsory(event.Compulsory) if event.UUID == "" { event.UUID = functions.GenerateUUID(event) } ev.SetUUID(event.UUID) return ev, nil } func (event *Event) ToModel() model.Event { return model.Event{ Day: event.GetDay(), Week: event.GetWeek(), Start: event.GetStart(), End: event.GetEnd(), Name: event.GetName(), EventType: event.GetEventType(), Prof: event.GetProf(), Rooms: event.GetRooms(), Notes: event.GetNotes(), BookedAt: event.GetBookedAt(), Course: event.GetCourse(), Semester: event.GetSemester(), UUID: event.GetUUID(), } } func (events *Events) ToEvents() model.Events { var result model.Events for _, event := range *events { result = append(result, event.ToModel()) } return result } func SaveSeminarGroupEvents(seminarGroup model.SeminarGroup, base *pocketbase.PocketBase) (model.Events, error) { return SaveEvents(seminarGroup.Events, base.App) } func SaveEvents(events []model.Event, app core.App) (model.Events, error) { var toBeSavedEvents model.Events var savedRecords model.Events // check if event is already in database and add to toBeSavedEvents if not for _, event := range events { existsInDatabase, err := findEventByDayWeekStartEndNameCourse(event, event.Course, app) alreadyAddedToSave := toBeSavedEvents.Contains(event) if err != nil { return nil, err } if !existsInDatabase && !alreadyAddedToSave { toBeSavedEvents = append(toBeSavedEvents, event) } } collection, err := app.FindCollectionByNameOrId("events") if err != nil { return nil, err } // create record for each event that's not already in the database for _, event := range toBeSavedEvents { // auto mapping for event fields to record fields savedEvent, saveErr := saveEvent(collection, event, app) if saveErr != nil { return nil, saveErr } else { savedRecords = append(savedRecords, savedEvent.ToModel()) } } return savedRecords, nil } func saveEvent(collection *core.Collection, event model.Event, app core.App) (*Event, error) { dbEvent, recordErr := NewEvent(collection, event) if recordErr != nil { return nil, recordErr } // auto mapping for event fields to record fields dbErr := app.Save(dbEvent) if dbErr != nil { return nil, dbErr } return dbEvent, nil } // check if event is already in database and return true if it is and false if it's not func findEventByDayWeekStartEndNameCourse(event model.Event, course string, app core.App) (bool, error) { var dbEvent model.Event err := app.DB().Select("*").From("events"). Where(dbx.NewExp( "Day = {:day} AND "+ "Week = {:week} AND "+ "Start = {:start} AND "+ "End = {:end} AND "+ "Name = {:name} AND "+ "course = {:course} AND "+ "Prof = {:prof} AND "+ "Rooms = {:rooms} AND "+ "EventType = {:eventType}", dbx.Params{ "day": event.Day, "week": event.Week, "start": event.Start, "end": event.End, "name": event.Name, "course": course, "prof": event.Prof, "rooms": event.Rooms, "eventType": event.EventType}), ).One(&dbEvent) if err != nil { if err.Error() == "sql: no rows in result set" { return false, nil } return false, err } else { return true, nil } } func buildIcalQueryForModules(modulesUuid []string) dbx.Expression { // check uuids against sql injection // uuids are generated by the system and are not user input // following the pattern of only containing alphanumeric characters and dashes for _, moduleUuid := range modulesUuid { err := uuid.Validate(moduleUuid) if err != nil { slog.Warn("Module UUID is not safe: ", "moduleUuid", moduleUuid) return dbx.HashExp{} } } // build where conditions for each module //first check if modules is empty if len(modulesUuid) == 0 { return dbx.HashExp{} } //second check if modules has only one element if len(modulesUuid) == 1 { return dbx.HashExp{"uuid": modulesUuid[0]} } //third check if modules has more than one element var wheres []dbx.Expression for _, moduleUuid := range modulesUuid { where := dbx.HashExp{"uuid": moduleUuid} wheres = append(wheres, where) } // Use dbx.And or dbx.Or to combine the where conditions as needed where := dbx.Or(wheres...) return where } // GetPlanForModules returns all events for the given modules with the given course // used for the ical feed func GetPlanForModules(app *pocketbase.PocketBase, modules []string) (model.Events, error) { var events model.Events // iterate over modules in 100 batch sizes for i := 0; i < len(modules); i += 100 { var moduleBatch []string if i+100 > len(modules) { moduleBatch = modules[i:] } else { moduleBatch = modules[i : i+100] } var selectedModulesQuery = buildIcalQueryForModules(moduleBatch) // get all events from event records in the events collection err := app.DB().Select("*").From("events").Where(selectedModulesQuery).OrderBy("start").All(&events) if err != nil { return nil, err } } return events, nil } func GetAllEventsForCourse(app *pocketbase.PocketBase, course string) (model.Events, error) { var events model.Events // get all events from event records in the events collection err := app.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.DB().Select("*").From("events").Where(dbx.NewExp("course = {:course} AND semester = {:semester}", dbx.Params{"course": course, "semester": semester})).GroupBy("Name").Distinct(true).All(&events) if err != nil { slog.Error("Error while getting events from database: ", "error", err) return nil, fmt.Errorf("error while getting events from database for course %s and semester %s", course, semester) } return events, nil } func GetAllModulesDistinctByNameAndCourse(app *pocketbase.PocketBase) ([]model.ModuleDTO, error) { var modules []model.ModuleDTO err := app.DB().Select("Name", "EventType", "Prof", "course", "semester", "uuid").From("events").GroupBy("Name", "Course").Distinct(true).All(&modules) if err != nil { slog.Error("Error while getting events from database: ", "error", err) return nil, fmt.Errorf("error while getting events distinct by name and course from data") } return modules, nil } func DeleteAllEventsByCourse(base *pocketbase.PocketBase, course string, semester string) error { _, err := base.DB().Delete("events", dbx.NewExp("course = {:course} AND semester = {:semester}", dbx.Params{"course": course, "semester": semester})).Execute() if err != nil { return err } return nil } func DeleteAllEventsRatherThenCourse(app core.App, course string, semester string) error { _, err := app.DB().Delete("events", dbx.NewExp("course != {:course} AND semester = {:semester}", dbx.Params{"course": course, "semester": semester})).Execute() if err != nil { return err } return nil } func DeleteAllEvents(app core.App) error { _, err := app.DB().Delete("events", dbx.NewExp("1=1")).Execute() if err != nil { return err } return nil } func FindModuleByUUID(app *pocketbase.PocketBase, uuid string) (model.Module, error) { var module model.Module err := app.DB().Select("*").From("events").Where(dbx.NewExp("uuid = {:uuid}", dbx.Params{"uuid": uuid})).One(&module) if err != nil { return model.Module{}, err } return module, nil } func FindAllEventsByModule(app *pocketbase.PocketBase, module model.Module) (model.Events, error) { var events model.Events err := app.DB().Select("*").From("events").Where(dbx.NewExp("Name = {:moduleName} AND course = {:course}", dbx.Params{"moduleName": module.Name, "course": module.Course})).All(&events) if err != nil { return nil, err } return events, nil } func GetAllModulesByNameAndDateRange(app *pocketbase.PocketBase, name string, startDate time.Time, endDate time.Time) (model.Events, error) { var events model.Events err := app.DB().Select("*").From("events").Where(dbx.NewExp("Name = {:name} AND Start >= {:startDate} AND End <= {:endDate}", dbx.Params{"name": name, "startDate": startDate, "endDate": endDate})).All(&events) if err != nil { return nil, err } return events, nil } // GetEventsThatCollideWithTimeRange returns all events that collide with the given time range // we have events with start and end in the database, we want to get all events that collide with the given time range // we have 4 cases: // 1. event starts before the given time range and ends after the given time range // 2. event starts after the given time range and ends before the given time range // 3. event starts before the given time range and ends before the given time range // 4. event starts after the given time range and ends after the given time range func GetEventsThatCollideWithTimeRange(app *pocketbase.PocketBase, from time.Time, to time.Time) (model.Events, error) { var fromTypeTime, _ = types.ParseDateTime(from) var toTypeTime, _ = types.ParseDateTime(to) events1, err := GetEventsThatStartBeforeAndEndAfter(app, fromTypeTime, toTypeTime) if err != nil { return nil, err } events2, err := GetEventsThatStartAfterAndEndBefore(app, fromTypeTime, toTypeTime) if err != nil { return nil, err } events3, err := GetEventsThatStartBeforeAndEndBefore(app, fromTypeTime, toTypeTime) if err != nil { return nil, err } events4, err := GetEventsThatStartAfterAndEndAfter(app, fromTypeTime, toTypeTime) if err != nil { return nil, err } var events model.Events events = append(events, events1...) events = append(events, events2...) events = append(events, events3...) events = append(events, events4...) return events, nil } func GetEventsThatStartBeforeAndEndAfter(app *pocketbase.PocketBase, from types.DateTime, to types.DateTime) (model.Events, error) { var events model.Events err := app.DB().Select("*").From("events").Where(dbx.NewExp("Start <= {:startDate} AND End >= {:endDate} AND Start <= {:endDate} AND End >= {:startDate}", dbx.Params{"startDate": from, "endDate": to})).Distinct(true).All(&events) if err != nil { return nil, err } return events, nil } func GetEventsThatStartAfterAndEndBefore(app *pocketbase.PocketBase, from types.DateTime, to types.DateTime) (model.Events, error) { var events model.Events err := app.DB().Select("*").From("events").Where(dbx.NewExp("Start >= {:startDate} AND End <= {:endDate} AND Start <= {:endDate} AND End >= {:startDate}", dbx.Params{"startDate": from, "endDate": to})).All(&events) if err != nil { return nil, err } return events, nil } func GetEventsThatStartBeforeAndEndBefore(app *pocketbase.PocketBase, from types.DateTime, to types.DateTime) (model.Events, error) { var events model.Events err := app.DB().Select("*").From("events").Where(dbx.NewExp("Start <= {:startDate} AND End <= {:endDate} AND Start <= {:endDate} AND End >= {:startDate}", dbx.Params{"startDate": from, "endDate": to})).All(&events) if err != nil { return nil, err } return events, nil } func GetAllEventTypes(app *pocketbase.PocketBase) ([]model.EventType, error) { var eventTypes []model.EventType err := app.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.DB().Select("*").From("events").Where(dbx.NewExp("Start >= {:startDate} AND End >= {:endDate} AND Start <= {:endDate} AND End >= {:startDate}", dbx.Params{"startDate": from, "endDate": to})).All(&events) if err != nil { return nil, err } return events, nil } func DeleteEvents(list model.Events, app *pocketbase.PocketBase) error { for _, event := range list { err := app.Delete(&event) if err != nil { return err } } return nil }