//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/pocketbase/pocketbase/daos" "github.com/pocketbase/pocketbase/tools/types" "htwkalender/data-manager/model" "log/slog" "time" "github.com/pocketbase/dbx" "github.com/pocketbase/pocketbase" ) func SaveSeminarGroupEvents(seminarGroup model.SeminarGroup, app *pocketbase.PocketBase) ([]model.Event, error) { var toBeSavedEvents model.Events var savedRecords model.Events // check if event is already in database and add to toBeSavedEvents if not for _, event := range seminarGroup.Events { event = event.SetCourse(seminarGroup.Course) existsInDatabase, err := findEventByDayWeekStartEndNameCourse(event, seminarGroup.Course, app.Dao()) alreadyAddedToSave := toBeSavedEvents.Contains(event) if err != nil { return nil, err } if !existsInDatabase && !alreadyAddedToSave { toBeSavedEvents = append(toBeSavedEvents, event) } } // create record for each event that's not already in the database for _, event := range toBeSavedEvents { event.MarkAsNew() // auto mapping for event fields to record fields err := app.Dao().Save(&event) if err != nil { return nil, err } else { savedRecords = append(savedRecords, event) } } return savedRecords, nil } func SaveEvents(events []model.Event, txDao *daos.Dao) ([]model.Event, error) { var toBeSavedEvents model.Events var savedRecords model.Events // check if event is already in database and add to toBeSavedEvents if not for _, event := range events { existsInDatabase, err := findEventByDayWeekStartEndNameCourse(event, event.Course, txDao) alreadyAddedToSave := toBeSavedEvents.Contains(event) if err != nil { return nil, err } if !existsInDatabase && !alreadyAddedToSave { toBeSavedEvents = append(toBeSavedEvents, event) } } // create record for each event that's not already in the database for _, event := range toBeSavedEvents { event.MarkAsNew() // auto mapping for event fields to record fields err := txDao.Save(&event) if err != nil { return nil, err } else { savedRecords = append(savedRecords, event) } } return savedRecords, nil } // check if event is already in database and return true if it is and false if it's not func findEventByDayWeekStartEndNameCourse(event model.Event, course string, dao *daos.Dao) (bool, error) { var dbEvent model.Event err := dao.DB().Select("*").From("events"). Where(dbx.NewExp( "Day = {:day} AND "+ "Week = {:week} AND "+ "Start = {:start} AND "+ "End = {:end} AND "+ "Name = {:name} AND "+ "course = {:course} AND "+ "Prof = {:prof} AND "+ "Rooms = {:rooms} AND "+ "EventType = {:eventType}", dbx.Params{ "day": event.Day, "week": event.Week, "start": event.Start, "end": event.End, "name": event.Name, "course": course, "prof": event.Prof, "rooms": event.Rooms, "eventType": event.EventType}), ).One(&dbEvent) if err != nil { if err.Error() == "sql: no rows in result set" { return false, nil } return false, err } else { return true, nil } } func buildIcalQueryForModules(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 { if !IsSafeIdentifier(moduleUuid) { 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 modulesArray := make([]string, 0, len(modules)) for _, value := range modules { modulesArray = append(modulesArray, value) } // iterate over modules in 100 batch sizes for i := 0; i < len(modules); i += 100 { var moduleBatch []string if i+100 > len(modules) { moduleBatch = modulesArray[i:] } else { moduleBatch = modulesArray[i : i+100] } var selectedModulesQuery = buildIcalQueryForModules(moduleBatch) // get all events from event records in the events collection err := app.Dao().DB().Select("*").From("events").Where(selectedModulesQuery).OrderBy("start").All(&events) if err != nil { return nil, err } } return events, nil } func GetAllEventsForCourse(app *pocketbase.PocketBase, course string) (model.Events, error) { var events model.Events // get all events from event records in the events collection err := app.Dao().DB().Select("*").From("events").Where(dbx.NewExp("course = {:course}", dbx.Params{"course": course})).All(&events) if err != nil { slog.Error("Error while getting events from database: ", "error", err) return nil, fmt.Errorf("error while getting events from database for course %s", course) } return events, nil } func GetAllModulesForCourse(app *pocketbase.PocketBase, course string, semester string) (model.Events, error) { var events model.Events // get all events from event records in the events collection err := app.Dao().DB().Select("*").From("events").Where(dbx.NewExp("course = {:course} AND semester = {:semester}", dbx.Params{"course": course, "semester": semester})).GroupBy("Name").Distinct(true).All(&events) if err != nil { slog.Error("Error while getting events from database: ", "error", err) return nil, fmt.Errorf("error while getting events from database for course %s and semester %s", course, semester) } return events, nil } func GetAllModulesDistinctByNameAndCourse(app *pocketbase.PocketBase) ([]model.ModuleDTO, error) { var modules []model.ModuleDTO err := app.Dao().DB().Select("Name", "EventType", "Prof", "course", "semester", "uuid").From("events").GroupBy("Name", "Course").Distinct(true).All(&modules) if err != nil { slog.Error("Error while getting events from database: ", "error", err) return nil, fmt.Errorf("error while getting events distinct by name and course from data") } return modules, nil } func DeleteAllEventsByCourse(dao *daos.Dao, course string, semester string) error { _, err := dao.DB().Delete("events", dbx.NewExp("course = {:course} AND semester = {:semester}", dbx.Params{"course": course, "semester": semester})).Execute() if err != nil { return err } return nil } func DeleteAllEventsRatherThenCourse(dao *daos.Dao, course string, semester string) error { _, err := dao.DB().Delete("events", dbx.NewExp("course != {:course} AND semester = {:semester}", dbx.Params{"course": course, "semester": semester})).Execute() if err != nil { return err } return nil } func DeleteAllEvents(app *pocketbase.PocketBase) error { _, err := app.Dao().DB().Delete("events", dbx.NewExp("1=1")).Execute() if err != nil { return err } return nil } func FindModuleByUUID(app *pocketbase.PocketBase, uuid string) (model.Module, error) { var module model.Module err := app.Dao().DB().Select("*").From("events").Where(dbx.NewExp("uuid = {:uuid}", dbx.Params{"uuid": uuid})).One(&module) if err != nil { return model.Module{}, err } return module, nil } func FindAllEventsByModule(app *pocketbase.PocketBase, module model.Module) (model.Events, error) { var events model.Events err := app.Dao().DB().Select("*").From("events").Where(dbx.NewExp("Name = {:moduleName} AND course = {:course}", dbx.Params{"moduleName": module.Name, "course": module.Course})).All(&events) if err != nil { return nil, err } return events, nil } func GetAllModulesByNameAndDateRange(app *pocketbase.PocketBase, name string, startDate time.Time, endDate time.Time) (model.Events, error) { var events model.Events err := app.Dao().DB().Select("*").From("events").Where(dbx.NewExp("Name = {:name} AND Start >= {:startDate} AND End <= {:endDate}", dbx.Params{"name": name, "startDate": startDate, "endDate": endDate})).All(&events) if err != nil { return nil, err } return events, nil } // GetEventsThatCollideWithTimeRange returns all events that collide with the given time range // we have events with start and end in the database, we want to get all events that collide with the given time range // we have 4 cases: // 1. event starts before the given time range and ends after the given time range // 2. event starts after the given time range and ends before the given time range // 3. event starts before the given time range and ends before the given time range // 4. event starts after the given time range and ends after the given time range func GetEventsThatCollideWithTimeRange(app *pocketbase.PocketBase, from time.Time, to time.Time) (model.Events, error) { var fromTypeTime, _ = types.ParseDateTime(from) var toTypeTime, _ = types.ParseDateTime(to) events1, err := GetEventsThatStartBeforeAndEndAfter(app, fromTypeTime, toTypeTime) if err != nil { return nil, err } events2, err := GetEventsThatStartAfterAndEndBefore(app, fromTypeTime, toTypeTime) if err != nil { return nil, err } events3, err := GetEventsThatStartBeforeAndEndBefore(app, fromTypeTime, toTypeTime) if err != nil { return nil, err } events4, err := GetEventsThatStartAfterAndEndAfter(app, fromTypeTime, toTypeTime) if err != nil { return nil, err } var events model.Events events = append(events, events1...) events = append(events, events2...) events = append(events, events3...) events = append(events, events4...) return events, nil } func GetEventsThatStartBeforeAndEndAfter(app *pocketbase.PocketBase, from types.DateTime, to types.DateTime) (model.Events, error) { var events model.Events err := app.Dao().DB().Select("*").From("events").Where(dbx.NewExp("Start <= {:startDate} AND End >= {:endDate} AND Start <= {:endDate} AND End >= {:startDate}", dbx.Params{"startDate": from, "endDate": to})).Distinct(true).All(&events) if err != nil { return nil, err } return events, nil } func GetEventsThatStartAfterAndEndBefore(app *pocketbase.PocketBase, from types.DateTime, to types.DateTime) (model.Events, error) { var events model.Events err := app.Dao().DB().Select("*").From("events").Where(dbx.NewExp("Start >= {:startDate} AND End <= {:endDate} AND Start <= {:endDate} AND End >= {:startDate}", dbx.Params{"startDate": from, "endDate": to})).All(&events) if err != nil { return nil, err } return events, nil } func GetEventsThatStartBeforeAndEndBefore(app *pocketbase.PocketBase, from types.DateTime, to types.DateTime) (model.Events, error) { var events model.Events err := app.Dao().DB().Select("*").From("events").Where(dbx.NewExp("Start <= {:startDate} AND End <= {:endDate} AND Start <= {:endDate} AND End >= {:startDate}", dbx.Params{"startDate": from, "endDate": to})).All(&events) if err != nil { return nil, err } return events, nil } func GetAllEventTypes(app *pocketbase.PocketBase) ([]model.EventType, error) { var eventTypes []model.EventType err := app.Dao().DB().Select("EventType").From("events").GroupBy("EventType").Distinct(true).All(&eventTypes) if err != nil { return nil, err } return eventTypes, nil } func GetEventsThatStartAfterAndEndAfter(app *pocketbase.PocketBase, from types.DateTime, to types.DateTime) (model.Events, error) { var events model.Events err := app.Dao().DB().Select("*").From("events").Where(dbx.NewExp("Start >= {:startDate} AND End >= {:endDate} AND Start <= {:endDate} AND End >= {:startDate}", dbx.Params{"startDate": from, "endDate": to})).All(&events) if err != nil { return nil, err } return events, nil } func DeleteEvents(list model.Events, app *pocketbase.PocketBase) error { for _, event := range list { err := app.Dao().Delete(&event) if err != nil { return err } } return nil }