Merge branch '150-fix-error-response' into 'main'

feat:#150 rewrote error handling for backend

Closes #2

See merge request ekresse/htwkalender!1
This commit is contained in:
ekresse
2024-01-23 18:22:57 +00:00
19 changed files with 759 additions and 694 deletions

View File

@@ -5,7 +5,7 @@ import (
"github.com/pocketbase/pocketbase/plugins/migratecmd"
_ "htwkalender/migrations"
"htwkalender/service"
"log"
"log/slog"
"os"
"strings"
)
@@ -27,6 +27,6 @@ func main() {
service.AddSchedules(app)
if err := app.Start(); err != nil {
log.Fatal(err)
slog.Error("Failed to start app: %v", err)
}
}

View File

@@ -0,0 +1,183 @@
package service
import (
"github.com/labstack/echo/v5"
"github.com/pocketbase/pocketbase"
"github.com/pocketbase/pocketbase/apis"
"github.com/pocketbase/pocketbase/core"
"htwkalender/service/db"
"htwkalender/service/ical"
"io"
"net/http"
)
func addFeedRoutes(app *pocketbase.PocketBase) {
app.OnBeforeServe().Add(func(e *core.ServeEvent) error {
_, err := e.Router.AddRoute(echo.Route{
Method: http.MethodPost,
Path: "/api/createFeed",
Handler: func(c echo.Context) error {
requestBody, _ := io.ReadAll(c.Request().Body)
result, err := ical.CreateIndividualFeed(requestBody, app)
if err != nil {
return c.JSON(http.StatusInternalServerError, "Failed to create individual feed")
}
return c.JSON(http.StatusOK, result)
},
Middlewares: []echo.MiddlewareFunc{
apis.ActivityLogger(app),
},
})
if err != nil {
return err
}
return nil
})
app.OnBeforeServe().Add(func(e *core.ServeEvent) error {
_, err := e.Router.AddRoute(echo.Route{
Method: http.MethodGet,
Path: "/api/feed",
Handler: func(c echo.Context) error {
token := c.QueryParam("token")
result, err := ical.Feed(app, token)
if err != nil {
return c.JSON(http.StatusInternalServerError, "Failed to get feed")
}
var responseWriter = c.Response().Writer
responseWriter.Header().Set("Content-type", "text/calendar")
responseWriter.Header().Set("charset", "utf-8")
responseWriter.Header().Set("Content-Disposition", "inline")
responseWriter.Header().Set("filename", "calendar.ics")
responseWriter.WriteHeader(http.StatusOK)
_, err = responseWriter.Write([]byte(result))
if err != nil {
return c.JSON(http.StatusInternalServerError, "Failed to write feed")
}
c.Response().Writer = responseWriter
return nil
},
Middlewares: []echo.MiddlewareFunc{
apis.ActivityLogger(app),
},
})
if err != nil {
return err
}
return nil
})
app.OnBeforeServe().Add(func(e *core.ServeEvent) error {
_, err := e.Router.AddRoute(echo.Route{
Method: http.MethodDelete,
Path: "/api/feed",
Handler: func(c echo.Context) error {
token := c.QueryParam("token")
err := db.DeleteFeed(app.Dao(), token)
if err != nil {
return c.JSON(http.StatusNotFound, err)
} else {
return c.JSON(http.StatusOK, "Feed deleted")
}
},
Middlewares: []echo.MiddlewareFunc{
apis.ActivityLogger(app),
},
})
if err != nil {
return err
}
return nil
})
app.OnBeforeServe().Add(func(e *core.ServeEvent) error {
_, err := e.Router.AddRoute(echo.Route{
Method: http.MethodPut,
Path: "/api/feed",
Handler: func(c echo.Context) error {
token := c.QueryParam("token")
return c.JSON(http.StatusOK, "token: "+token)
},
Middlewares: []echo.MiddlewareFunc{
apis.ActivityLogger(app),
},
})
if err != nil {
return err
}
return nil
})
// Route for Thunderbird to get calendar server information
// Response is a 200 OK without additional content
app.OnBeforeServe().Add(func(e *core.ServeEvent) error {
_, err := e.Router.AddRoute(echo.Route{
Method: "PROPFIND",
Path: "/",
Handler: func(c echo.Context) error {
return c.JSON(http.StatusOK, "")
},
Middlewares: []echo.MiddlewareFunc{
apis.ActivityLogger(app),
},
})
if err != nil {
return err
}
return nil
})
// Route for Thunderbird to get calendar server information
// Response is a 200 OK without additional content
app.OnBeforeServe().Add(func(e *core.ServeEvent) error {
_, err := e.Router.AddRoute(echo.Route{
Method: "PROPFIND",
Path: "/.well-known/caldav",
Handler: func(c echo.Context) error {
return c.JSON(http.StatusOK, "")
},
Middlewares: []echo.MiddlewareFunc{
apis.ActivityLogger(app),
},
})
if err != nil {
return err
}
return nil
})
app.OnBeforeServe().Add(func(e *core.ServeEvent) error {
_, err := e.Router.AddRoute(echo.Route{
Method: "PROPFIND",
Path: "/api/feed",
Handler: func(c echo.Context) error {
return c.JSON(http.StatusOK, "")
},
Middlewares: []echo.MiddlewareFunc{
apis.ActivityLogger(app),
},
})
if err != nil {
return err
}
return nil
})
app.OnBeforeServe().Add(func(e *core.ServeEvent) error {
_, err := e.Router.AddRoute(echo.Route{
Method: http.MethodHead,
Path: "/api/feed",
Handler: func(c echo.Context) error {
return c.JSON(http.StatusOK, "")
},
Middlewares: []echo.MiddlewareFunc{
apis.ActivityLogger(app),
},
})
if err != nil {
return err
}
return nil
})
}

View File

@@ -1,14 +1,13 @@
package service
import (
"htwkalender/service/db"
"htwkalender/service/events"
"htwkalender/service/fetch/sport"
v1 "htwkalender/service/fetch/v1"
v2 "htwkalender/service/fetch/v2"
"htwkalender/service/ical"
"htwkalender/service/room"
"io"
"log/slog"
"net/http"
"github.com/labstack/echo/v5"
@@ -24,7 +23,13 @@ func AddRoutes(app *pocketbase.PocketBase) {
Method: http.MethodGet,
Path: "/api/fetch/events",
Handler: func(c echo.Context) error {
return v2.ParseEventsFromRemote(c, app)
savedEvents, err := v2.ParseEventsFromRemote(app)
if err != nil {
slog.Error("Failed to parse events from remote: %v", err)
return c.JSON(http.StatusBadRequest, "Failed to parse events from remote")
} else {
return c.JSON(http.StatusOK, savedEvents)
}
},
Middlewares: []echo.MiddlewareFunc{
apis.ActivityLogger(app),
@@ -42,7 +47,11 @@ func AddRoutes(app *pocketbase.PocketBase) {
Method: http.MethodGet,
Path: "/api/fetch/groups",
Handler: func(c echo.Context) error {
return v1.SeminarGroups(c, app)
groups, err := v1.FetchSeminarGroups(app)
if err != nil {
return c.JSON(http.StatusBadRequest, "Failed to fetch seminar groups")
}
return c.JSON(http.StatusOK, groups)
},
Middlewares: []echo.MiddlewareFunc{
apis.ActivityLogger(app),
@@ -61,8 +70,11 @@ func AddRoutes(app *pocketbase.PocketBase) {
Path: "/api/fetch/sports",
Handler: func(c echo.Context) error {
sportEvents := sport.FetchAndUpdateSportEvents(app)
return c.JSON(200, sportEvents)
sportEvents, err := sport.FetchAndUpdateSportEvents(app)
if err != nil {
return c.JSON(http.StatusBadRequest, "Failed to fetch sport events")
}
return c.JSON(http.StatusOK, sportEvents)
},
Middlewares: []echo.MiddlewareFunc{
apis.ActivityLogger(app),
@@ -80,7 +92,11 @@ func AddRoutes(app *pocketbase.PocketBase) {
Method: http.MethodDelete,
Path: "/api/modules",
Handler: func(c echo.Context) error {
return events.DeleteAllEvents(app)
err := events.DeleteAllEvents(app)
if err != nil {
return c.JSON(http.StatusBadRequest, "Failed to delete events")
}
return c.JSON(http.StatusOK, "Events deleted")
},
Middlewares: []echo.MiddlewareFunc{
apis.ActivityLogger(app),
@@ -98,7 +114,11 @@ func AddRoutes(app *pocketbase.PocketBase) {
Method: http.MethodGet,
Path: "/api/rooms",
Handler: func(c echo.Context) error {
return room.GetRooms(c, app)
rooms, err := room.GetRooms(app)
if err != nil {
return c.JSON(http.StatusBadRequest, "Failed to get rooms")
}
return c.JSON(http.StatusOK, rooms)
},
Middlewares: []echo.MiddlewareFunc{
apis.ActivityLogger(app),
@@ -118,7 +138,12 @@ func AddRoutes(app *pocketbase.PocketBase) {
Handler: func(c echo.Context) error {
roomParam := c.QueryParam("room")
date := c.QueryParam("date")
return room.GetRoomScheduleForDay(c, app, roomParam, date)
roomSchedule, err := room.GetRoomScheduleForDay(app, roomParam, date)
if err != nil {
slog.Error("Failed to get room schedule for day: %v", err)
return c.JSON(http.StatusBadRequest, "Failed to get room schedule for day")
}
return c.JSON(http.StatusOK, roomSchedule)
},
Middlewares: []echo.MiddlewareFunc{
apis.ActivityLogger(app),
@@ -139,30 +164,12 @@ func AddRoutes(app *pocketbase.PocketBase) {
roomParam := c.QueryParam("room")
to := c.QueryParam("to")
from := c.QueryParam("from")
return room.GetRoomSchedule(c, app, roomParam, from, to)
},
Middlewares: []echo.MiddlewareFunc{
apis.ActivityLogger(app),
},
})
if err != nil {
return err
}
return nil
})
app.OnBeforeServe().Add(func(e *core.ServeEvent) error {
_, err := e.Router.AddRoute(echo.Route{
Method: http.MethodPost,
Path: "/api/createFeed",
Handler: func(c echo.Context) error {
requestBody, _ := io.ReadAll(c.Request().Body)
result, err := ical.CreateIndividualFeed(requestBody, app)
roomSchedule, err := room.GetRoomSchedule(app, roomParam, from, to)
if err != nil {
return c.JSON(http.StatusInternalServerError, err)
slog.Error("Failed to get room schedule: %v", err)
return c.JSON(http.StatusBadRequest, "Failed to get room schedule")
}
return c.JSON(http.StatusOK, result)
return c.JSON(http.StatusOK, roomSchedule)
},
Middlewares: []echo.MiddlewareFunc{
apis.ActivityLogger(app),
@@ -174,64 +181,7 @@ 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/feed",
Handler: func(c echo.Context) error {
token := c.QueryParam("token")
return ical.Feed(c, app, token)
},
Middlewares: []echo.MiddlewareFunc{
apis.ActivityLogger(app),
},
})
if err != nil {
return err
}
return nil
})
app.OnBeforeServe().Add(func(e *core.ServeEvent) error {
_, err := e.Router.AddRoute(echo.Route{
Method: http.MethodDelete,
Path: "/api/feed",
Handler: func(c echo.Context) error {
token := c.QueryParam("token")
err := db.DeleteFeed(app.Dao(), token)
if err != nil {
return c.JSON(http.StatusNotFound, err)
} else {
return c.JSON(http.StatusOK, "Feed deleted")
}
},
Middlewares: []echo.MiddlewareFunc{
apis.ActivityLogger(app),
},
})
if err != nil {
return err
}
return nil
})
app.OnBeforeServe().Add(func(e *core.ServeEvent) error {
_, err := e.Router.AddRoute(echo.Route{
Method: http.MethodPut,
Path: "/api/feed",
Handler: func(c echo.Context) error {
token := c.QueryParam("token")
return c.JSON(http.StatusOK, "token: "+token)
},
Middlewares: []echo.MiddlewareFunc{
apis.ActivityLogger(app),
},
})
if err != nil {
return err
}
return nil
})
addFeedRoutes(app)
app.OnBeforeServe().Add(func(e *core.ServeEvent) error {
_, err := e.Router.AddRoute(echo.Route{
@@ -243,9 +193,10 @@ func AddRoutes(app *pocketbase.PocketBase) {
modules, err := events.GetModulesForCourseDistinct(app, course, semester)
if err != nil {
return c.JSON(400, err)
slog.Error("Failed to get modules for course and semester: %v", err)
return c.JSON(http.StatusBadRequest, "Failed to get modules for course and semester")
} else {
return c.JSON(200, modules)
return c.JSON(http.StatusOK, modules)
}
},
Middlewares: []echo.MiddlewareFunc{
@@ -263,7 +214,12 @@ func AddRoutes(app *pocketbase.PocketBase) {
Method: http.MethodGet,
Path: "/api/modules",
Handler: func(c echo.Context) error {
return events.GetAllModulesDistinct(app, c)
modules, err := events.GetAllModulesDistinct(app)
if err != nil {
slog.Error("Failed to get modules: %v", err)
return c.JSON(http.StatusBadRequest, "Failed to get modules")
}
return c.JSON(http.StatusOK, modules)
},
Middlewares: []echo.MiddlewareFunc{
apis.ActivityLogger(app),
@@ -283,9 +239,10 @@ func AddRoutes(app *pocketbase.PocketBase) {
requestModule := c.QueryParam("uuid")
module, err := events.GetModuleByUUID(app, requestModule)
if err != nil {
return c.JSON(400, err)
slog.Error("Failed to get module: %v", err)
return c.JSON(http.StatusBadRequest, "Failed to get module")
} else {
return c.JSON(200, module)
return c.JSON(http.StatusOK, module)
}
},
Middlewares: []echo.MiddlewareFunc{
@@ -325,9 +282,10 @@ func AddRoutes(app *pocketbase.PocketBase) {
semester := c.QueryParam("semester")
err := events.DeleteAllEventsByCourseAndSemester(app, course, semester)
if err != nil {
return c.JSON(400, err)
slog.Error("Failed to delete events: %v", err)
return c.JSON(http.StatusBadRequest, "Failed to delete events")
} else {
return c.JSON(200, "Events deleted")
return c.JSON(http.StatusBadRequest, "Events deleted")
}
},
Middlewares: []echo.MiddlewareFunc{
@@ -349,9 +307,10 @@ func AddRoutes(app *pocketbase.PocketBase) {
err := ical.MigrateFeedJson(app)
if err != nil {
return c.JSON(500, err)
slog.Error("Failed to migrate feeds: %v", err)
return c.JSON(http.StatusInternalServerError, "Failed to migrate feeds")
} else {
return c.JSON(200, "Migrated")
return c.JSON(http.StatusOK, "Migrated")
}
},
Middlewares: []echo.MiddlewareFunc{

View File

@@ -10,7 +10,7 @@ import (
"htwkalender/service/fetch/sport"
v2 "htwkalender/service/fetch/v2"
"htwkalender/service/functions/time"
"log"
"log/slog"
"strconv"
)
@@ -23,38 +23,42 @@ func AddSchedules(app *pocketbase.PocketBase) {
// 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() {
slog.Info("Started updating courses schedule")
course.UpdateCourse(app)
})
// Every sunday at 1am clean all courses (5 segments - minute, hour, day, month, weekday) "0 3 * * 0"
scheduler.MustAdd("cleanFeeds", "0 1 * * 0", func() {
// clean feeds older than 6 months
slog.Info("Started cleaning feeds schedule")
feed.ClearFeeds(app.Dao(), 6, time.RealClock{})
})
// Every sunday at 3am fetch all sport events (5 segments - minute, hour, day, month, weekday) "0 2 * * 0"
// Every sunday at 2am fetch all sport events (5 segments - minute, hour, day, month, weekday) "0 2 * * 0"
scheduler.MustAdd("fetchSportEvents", "0 3 * * 0", func() {
sport.FetchAndUpdateSportEvents(app)
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.Info("Successfully fetched " + strconv.FormatInt(int64(len(sportEvents)), 10) + " sport events")
})
//delete all events and then fetch all events from remote this should be done every sunday at 2am
scheduler.MustAdd("fetchEvents", "0 2 * * 0", func() {
err := events.DeleteAllEvents(app)
if err != nil {
log.Println(err)
slog.Error("Failed to delete all events: %v", err)
}
err, savedEvents := v2.FetchAllEventsAndSave(app)
savedEvents, err := v2.FetchAllEventsAndSave(app, time.RealClock{})
if err != nil {
log.Println(err)
slog.Error("Failed to fetch and save events: %v", err)
} else {
log.Println("Successfully saved: " + strconv.FormatInt(int64(len(savedEvents)), 10) + " events")
slog.Info("Successfully fetched " + strconv.FormatInt(int64(len(savedEvents)), 10) + " events")
}
})
scheduler.Start()
return nil
})
}

View File

@@ -3,18 +3,18 @@ package course
import (
"github.com/pocketbase/pocketbase"
"htwkalender/service/events"
"log"
"log/slog"
"strconv"
)
func UpdateCourse(app *pocketbase.PocketBase) {
courses := events.GetAllCourses(app)
for _, course := range courses {
err := events.UpdateModulesForCourse(app, course)
savedEvents, err := events.UpdateModulesForCourse(app, course)
if err != nil {
log.Println("Update Course: " + course + " failed")
log.Println(err)
slog.Warn("Update Course: "+course+" failed: %v", err)
} else {
log.Println("Update Course: " + course + " successful")
slog.Info("Updated Course: " + course + " with " + strconv.FormatInt(int64(len(savedEvents)), 10) + " events")
}
}
}

View File

@@ -1,7 +1,9 @@
package db
import (
"fmt"
"htwkalender/model"
"log/slog"
"time"
"github.com/pocketbase/dbx"
@@ -145,7 +147,7 @@ func buildIcalQueryForModules(modules []model.FeedCollection) dbx.Expression {
// GetPlanForModules returns all events for the given modules with the given course
// used for the ical feed
func GetPlanForModules(app *pocketbase.PocketBase, modules map[string]model.FeedCollection) model.Events {
func GetPlanForModules(app *pocketbase.PocketBase, modules map[string]model.FeedCollection) (model.Events, error) {
var events model.Events
@@ -168,12 +170,11 @@ func GetPlanForModules(app *pocketbase.PocketBase, modules map[string]model.Feed
// get all events from event records in the events collection
err := app.Dao().DB().Select("*").From("events").Where(selectedModulesQuery).All(&events)
if err != nil {
print("Error while getting events from database: ", err)
return nil
return nil, err
}
}
return events
return events, nil
}
func GetAllModulesForCourse(app *pocketbase.PocketBase, course string, semester string) (model.Events, error) {
@@ -182,8 +183,8 @@ func GetAllModulesForCourse(app *pocketbase.PocketBase, course string, semester
// 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 {
print("Error while getting events from database: ", err)
return nil, err
slog.Error("Error while getting events from database: ", err)
return nil, fmt.Errorf("error while getting events from database for course %s and semester %s", course, semester)
}
return events, nil
@@ -194,8 +195,8 @@ 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 {
print("Error while getting events from database: ", err)
return nil, err
slog.Error("Error while getting events from database: ", err)
return nil, fmt.Errorf("error while getting events distinct by name and course from data")
}
return modules, nil
@@ -205,10 +206,8 @@ func DeleteAllEventsForCourse(app *pocketbase.PocketBase, course string, semeste
_, err := app.Dao().DB().Delete("events", dbx.NewExp("course = {:course} AND semester = {:semester}", dbx.Params{"course": course, "semester": semester})).Execute()
if err != nil {
print("Error while deleting events from database: ", err)
return err
}
return nil
}
@@ -228,7 +227,6 @@ func FindModuleByUUID(app *pocketbase.PocketBase, uuid string) (model.Module, er
err := app.Dao().DB().Select("*").From("events").Where(dbx.NewExp("uuid = {:uuid}", dbx.Params{"uuid": uuid})).One(&module)
if err != nil {
print("Error while getting events from database: ", err)
return model.Module{}, err
}
@@ -240,7 +238,6 @@ func FindAllEventsByModule(app *pocketbase.PocketBase, module model.Module) (mod
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 {
print("Error while getting events from database: ", err)
return nil, err
}
@@ -252,7 +249,6 @@ func GetAllModulesByNameAndDateRange(app *pocketbase.PocketBase, name string, st
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 {
print("Error while getting events from database: ", err)
return nil, err
}

View File

@@ -5,6 +5,7 @@ import (
"github.com/pocketbase/pocketbase"
"github.com/pocketbase/pocketbase/models"
"htwkalender/model"
"log/slog"
)
func SaveGroups(seminarGroup []model.SeminarGroup, collection *models.Collection, app *pocketbase.PocketBase) ([]*models.Record, error) {
@@ -67,8 +68,8 @@ 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 {
print("Error while getting groups from database: ", err)
return nil
slog.Error("Error while getting groups from database: ", err)
return []string{}
}
var courseArray []string

View File

@@ -1,7 +1,6 @@
package db
import (
"fmt"
"htwkalender/model"
"htwkalender/service/functions"
"strings"
@@ -11,7 +10,7 @@ import (
"github.com/pocketbase/pocketbase"
)
func GetRooms(app *pocketbase.PocketBase) []string {
func GetRooms(app *pocketbase.PocketBase) ([]string, error) {
var events []struct {
Rooms string `db:"Rooms" json:"Rooms"`
@@ -21,22 +20,25 @@ func GetRooms(app *pocketbase.PocketBase) []string {
// get all rooms from event records in the events collection
err := app.Dao().DB().Select("Rooms", "course").From("events").Distinct(true).All(&events)
if err != nil {
print("Error while getting rooms from database: ", err)
return nil
return nil, err
}
roomArray := clearAndSeparateRooms([]struct {
roomArray, err := clearAndSeparateRooms([]struct {
Rooms string
Course string
}(events))
return roomArray
if err != nil {
return nil, err
}
return roomArray, nil
}
func clearAndSeparateRooms(events []struct {
Rooms string
Course string
}) []string {
}) ([]string, error) {
var roomArray []string
for _, event := range events {
@@ -60,10 +62,10 @@ func clearAndSeparateRooms(events []struct {
}
}
}
return roomArray
return roomArray, nil
}
func GetRoomScheduleForDay(app *pocketbase.PocketBase, room string, date string) []model.Event {
func GetRoomScheduleForDay(app *pocketbase.PocketBase, room string, date string) ([]model.Event, error) {
var events []model.Event
// get all events from event records in the events collection
@@ -73,24 +75,21 @@ func GetRoomScheduleForDay(app *pocketbase.PocketBase, room string, date string)
GroupBy("Week", "Start", "End", "Rooms").
All(&events)
if err != nil {
print("Error while getting events from database: ", err)
return nil
return nil, err
}
return events
return events, nil
}
func GetRoomSchedule(app *pocketbase.PocketBase, room string, from string, to string) []model.Event {
func GetRoomSchedule(app *pocketbase.PocketBase, room string, from string, to string) ([]model.Event, error) {
var events []model.Event
fromDate, err := time.Parse("2006-01-02", from)
if err != nil {
fmt.Println("Error parsing date 'from':", err)
return nil
return nil, err
}
toDate, err := time.Parse("2006-01-02", to)
if err != nil {
fmt.Println("Error parsing date 'to':", err)
return nil
return nil, err
}
// get all events from event records in the events collection
@@ -101,8 +100,7 @@ func GetRoomSchedule(app *pocketbase.PocketBase, room string, from string, to st
All(&events)
if err != nil {
print("Error while getting events from database: ", err)
return nil
return nil, err
}
return events
return events, nil
}

View File

@@ -1,9 +1,7 @@
package events
import (
"github.com/labstack/echo/v5"
"github.com/pocketbase/pocketbase"
"github.com/pocketbase/pocketbase/apis"
"htwkalender/model"
"htwkalender/service/db"
"htwkalender/service/fetch/v1"
@@ -41,21 +39,17 @@ func replaceEmptyEntry(namedList []Named, replacement string) {
// GetAllModulesDistinct returns all modules distinct by name and course from the database
// That means you get all modules with duplicates if they have different courses
func GetAllModulesDistinct(app *pocketbase.PocketBase, c echo.Context) error {
func GetAllModulesDistinct(app *pocketbase.PocketBase) ([]model.ModuleDTO, error) {
modules, err := db.GetAllModulesDistinctByNameAndCourse(app)
if err != nil {
return nil, err
}
var namedModules []Named
for _, module := range modules {
namedModules = append(namedModules, &module)
}
replaceEmptyEntry(namedModules, "Sonderveranstaltungen")
if err != nil {
return c.JSON(400, err)
} else {
return c.JSON(200, modules)
}
return modules, nil
}
func GetModuleByUUID(app *pocketbase.PocketBase, uuid string) (model.Module, error) {
@@ -105,7 +99,7 @@ func DeleteAllEvents(app *pocketbase.PocketBase) error {
// 3. Save all events for the course and the semester
// If the update was successful, nil is returned
// If the update was not successful, an error is returned
func UpdateModulesForCourse(app *pocketbase.PocketBase, course string) error {
func UpdateModulesForCourse(app *pocketbase.PocketBase, course string) (model.Events, error) {
//new string array with one element (course)
var courses []string
@@ -125,29 +119,32 @@ func UpdateModulesForCourse(app *pocketbase.PocketBase, course string) error {
//get all events for the course and the semester
events, err := db.GetAllModulesForCourse(app, course, "ws")
if err != nil {
return apis.NewNotFoundError("Events for winter semester could not be found", err)
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 apis.NewNotFoundError("Events for summer semester could not be found", err)
return nil, err
}
events = append(events, summerEvents...)
//if there are no events in the database, save the new events
if len(events) == 0 {
_, dbError := db.SaveSeminarGroupEvents(seminarGroups, app)
events, dbError := db.SaveSeminarGroupEvents(seminarGroups, app)
if dbError != nil {
return apis.NewNotFoundError("Events could not be saved", dbError)
return nil, dbError
}
return nil
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
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
@@ -155,25 +152,24 @@ func UpdateModulesForCourse(app *pocketbase.PocketBase, course string) error {
err = DeleteAllEventsByCourseAndSemester(app, course, "ws")
if err != nil {
return err
return nil, err
}
err = DeleteAllEventsByCourseAndSemester(app, course, "ss")
if err != nil {
return err
return nil, err
}
//save the new events
_, dbError := db.SaveSeminarGroupEvents(seminarGroups, app)
savedEvent, dbError := db.SaveSeminarGroupEvents(seminarGroups, app)
if dbError != nil {
return apis.NewNotFoundError("Events could not be saved", dbError)
return nil, dbError
}
return nil
savedEvents = append(savedEvents, savedEvent...)
}
}
}
return nil
return savedEvents, nil
}
func ContainsEvent(events model.Events, event model.Event) bool {

View File

@@ -1,17 +1,18 @@
package feed
import (
"database/sql"
"github.com/pocketbase/dbx"
"github.com/pocketbase/pocketbase/daos"
database "htwkalender/service/db"
localTime "htwkalender/service/functions/time"
"log"
"log/slog"
)
func ClearFeeds(db *daos.Dao, months int, clock localTime.Clock) {
feeds, err := database.GetAllFeeds(db)
if err != nil {
log.Println("CleanFeeds: get all feeds failed")
slog.Error("CleanFeeds: failed to get all feeds", err)
return
}
for _, feed := range feeds {
@@ -22,13 +23,13 @@ func ClearFeeds(db *daos.Dao, months int, clock localTime.Clock) {
if feedRetrievedTime.Before(timeShift) {
// delete feed
sqlResult, err := db.DB().Delete("feeds", dbx.NewExp("id = {:id}", dbx.Params{"id": feed.GetId()})).Execute()
var sqlResult sql.Result
sqlResult, err = db.DB().Delete("feeds", dbx.NewExp("id = {:id}", dbx.Params{"id": feed.GetId()})).Execute()
if err != nil {
log.Println("CleanFeeds: delete feed " + feed.GetId() + " failed")
log.Println(err)
log.Println(sqlResult)
slog.Error("CleanFeeds: delete feed "+feed.GetId()+" failed", err)
slog.Error("SQL Result: ", sqlResult)
} else {
log.Println("CleanFeeds: delete feed " + feed.GetId() + " successful")
slog.Info("CleanFeeds: delete feed " + feed.GetId() + " successful")
}
}
}

View File

@@ -8,6 +8,8 @@ import (
"htwkalender/model"
"htwkalender/service/db"
"htwkalender/service/functions"
"io"
"log/slog"
"net/http"
"regexp"
"strconv"
@@ -21,9 +23,14 @@ import (
// FetchAndUpdateSportEvents fetches all sport events from the HTWK sport website
// it deletes them first and then saves them to the database
// It returns all saved events
func FetchAndUpdateSportEvents(app *pocketbase.PocketBase) []model.Event {
func FetchAndUpdateSportEvents(app *pocketbase.PocketBase) ([]model.Event, error) {
sportCourseLinks, err := fetchAllAvailableSportCourses()
if err != nil {
return nil, err
}
var sportCourseLinks = fetchAllAvailableSportCourses()
sportEntries := fetchHTWKSportCourses(sportCourseLinks)
events := formatEntriesToEvents(sportEntries)
@@ -43,7 +50,7 @@ func FetchAndUpdateSportEvents(app *pocketbase.PocketBase) []model.Event {
// get all events from database where name = Feiertage und lehrveranstaltungsfreie Tage
holidays, err := db.GetAllModulesByNameAndDateRange(app, "Feiertage und lehrveranstaltungsfreie Tage", earliestDate, latestDate)
if err != nil {
return nil
return nil, err
}
// remove all events that have same year, month and day as items in holidays
@@ -60,17 +67,17 @@ func FetchAndUpdateSportEvents(app *pocketbase.PocketBase) []model.Event {
// @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.DeleteAllEventsForCourse(app, "Sport", functions.GetCurrentSemesterString())
if err != nil {
return nil
return nil, err
}
// save events to database
savedEvents, err := db.SaveEvents(events, app)
if err != nil {
return nil
return nil, err
}
return savedEvents
return savedEvents, nil
}
@@ -305,13 +312,14 @@ func checkSemester(date time.Time) string {
}
// fetch the main page where all sport courses are listed and extract all links to the sport courses
func fetchAllAvailableSportCourses() []string {
func fetchAllAvailableSportCourses() ([]string, error) {
var url = "https://sport.htwk-leipzig.de/sportangebote"
var doc, err = htmlRequest(url)
if err != nil {
return nil
slog.Error("Error while fetching sport courses from webpage", err)
return nil, err
}
// link list of all sport courses
@@ -325,7 +333,7 @@ func fetchAllAvailableSportCourses() []string {
}
})
return links
return links, nil
}
// fetchAllHTWKSportCourses fetches all sport courses from the given links.
@@ -372,7 +380,13 @@ func htmlRequest(url string) (*goquery.Document, error) {
if err != nil {
return nil, err
}
defer resp.Body.Close()
defer func(Body io.ReadCloser) {
readErr := Body.Close()
if readErr != nil {
slog.Error("Error while closing response body from html request", readErr)
return
}
}(resp.Body)
doc, err := goquery.NewDocumentFromReader(resp.Body)
if err != nil {

View File

@@ -2,43 +2,18 @@ package v1
import (
"fmt"
"github.com/google/uuid"
"github.com/pocketbase/pocketbase/tools/types"
"golang.org/x/net/html"
"htwkalender/model"
"htwkalender/service/date"
"htwkalender/service/db"
"htwkalender/service/fetch"
"net/http"
"regexp"
"strconv"
"strings"
"time"
"github.com/google/uuid"
"github.com/labstack/echo/v5"
"github.com/pocketbase/pocketbase"
"github.com/pocketbase/pocketbase/apis"
"github.com/pocketbase/pocketbase/tools/types"
"golang.org/x/net/html"
)
func GetSeminarEvents(c echo.Context, app *pocketbase.PocketBase) error {
seminarGroupsLabel := db.GetAllCourses(app)
seminarGroups := GetSeminarGroupsEventsFromHTML(seminarGroupsLabel)
seminarGroups = ClearEmptySeminarGroups(seminarGroups)
seminarGroups = ReplaceEmptyEventNames(seminarGroups)
savedRecords, dbError := db.SaveSeminarGroupEvents(seminarGroups, app)
if dbError != nil {
return apis.NewNotFoundError("Events could not be saved", dbError.Error())
}
return c.JSON(http.StatusOK, savedRecords)
}
func ReplaceEmptyEventNames(groups []model.SeminarGroup) []model.SeminarGroup {
for i, group := range groups {
for j, event := range group.Events {

View File

@@ -3,13 +3,12 @@ package v1
import (
"encoding/xml"
"fmt"
"github.com/labstack/echo/v5"
"github.com/pocketbase/pocketbase"
"github.com/pocketbase/pocketbase/apis"
"github.com/pocketbase/pocketbase/models"
"htwkalender/model"
"htwkalender/service/db"
"io"
"log/slog"
"net/http"
)
@@ -41,12 +40,23 @@ func getSeminarHTML(semester string) (string, error) {
}
func SeminarGroups(c echo.Context, app *pocketbase.PocketBase) error {
func FetchSeminarGroups(app *pocketbase.PocketBase) ([]*models.Record, error) {
var groups []model.SeminarGroup
resultSummer, _ := getSeminarHTML("ss")
resultSummer, err := getSeminarHTML("ss")
if err != nil {
slog.Error("Error while fetching seminar groups for winter semester", err)
return nil, err
}
resultWinter, _ := getSeminarHTML("ws")
if err != nil {
slog.Error("Error while fetching seminar groups for summer semester", err)
return nil, err
}
groups = parseSeminarGroups(resultSummer)
groups = append(groups, parseSeminarGroups(resultWinter)...)
@@ -55,16 +65,18 @@ func SeminarGroups(c echo.Context, app *pocketbase.PocketBase) error {
collection, dbError := db.FindCollection(app, "groups")
if dbError != nil {
return apis.NewNotFoundError("Collection not found", dbError)
slog.Error("Error while searching collection groups", dbError)
return nil, err
}
var insertedGroups []*models.Record
insertedGroups, dbError = db.SaveGroups(groups, collection, app)
if dbError != nil {
return apis.NewNotFoundError("Records could not be saved", dbError)
slog.Error("Error while saving groups", dbError)
return nil, err
}
return c.JSON(http.StatusOK, insertedGroups)
return insertedGroups, nil
}
func removeDuplicates(groups []model.SeminarGroup) []model.SeminarGroup {

View File

@@ -1,34 +1,27 @@
package v2
import (
"fmt"
"github.com/google/uuid"
"github.com/labstack/echo/v5"
"github.com/pocketbase/pocketbase"
"golang.org/x/net/html"
"htwkalender/model"
"htwkalender/service/db"
"htwkalender/service/fetch"
"strconv"
localTime "htwkalender/service/functions/time"
"strings"
"time"
)
func ParseEventsFromRemote(c echo.Context, app *pocketbase.PocketBase) error {
err, savedRecords := FetchAllEventsAndSave(app)
func ParseEventsFromRemote(app *pocketbase.PocketBase) (model.Events, error) {
savedRecords, err := FetchAllEventsAndSave(app, localTime.RealClock{})
if err != nil {
return err
} else {
savedRecordsLength := strconv.FormatInt(int64(len(savedRecords)), 10)
return c.JSON(200, "Successfully saved "+savedRecordsLength+" events")
return nil, err
}
return savedRecords, nil
}
func FetchAllEventsAndSave(app *pocketbase.PocketBase) (error, []model.Event) {
var err error
func FetchAllEventsAndSave(app *pocketbase.PocketBase, clock localTime.Clock) ([]model.Event, error) {
var savedRecords []model.Event
var events []model.Event
var stubUrl = [2]string{
"https://stundenplan.htwk-leipzig.de/",
@@ -55,22 +48,32 @@ func FetchAllEventsAndSave(app *pocketbase.PocketBase) (error, []model.Event) {
"%0A?&template=sws_modul&weeks=1-65&combined=yes",
}
if (time.Now().Month() >= 3) && (time.Now().Month() <= 10) {
if (clock.Now().Month() >= 3) && (clock.Now().Month() <= 10) {
url := stubUrl[0] + "ss" + stubUrl[1]
events, err = parseEventForOneSemester(url)
events, err := parseEventForOneSemester(url)
if err != nil {
return nil, fmt.Errorf("failed to parse events for summmer semester: %w", err)
}
savedEvents, dbError := db.SaveEvents(events, app)
err = dbError
savedRecords = append(savedRecords, savedEvents...)
if dbError != nil {
return nil, fmt.Errorf("failed to save events: %w", dbError)
}
savedRecords = append(savedEvents, events...)
}
if (time.Now().Month() >= 9) || (time.Now().Month() <= 4) {
if (clock.Now().Month() >= 9) || (clock.Now().Month() <= 4) {
url := stubUrl[0] + "ws" + stubUrl[1]
events, err = parseEventForOneSemester(url)
events, err := parseEventForOneSemester(url)
if err != nil {
return nil, fmt.Errorf("failed to parse events for winter semester: %w", err)
}
savedEvents, dbError := db.SaveEvents(events, app)
err = dbError
if dbError != nil {
return nil, fmt.Errorf("failed to save events: %w", dbError)
}
savedRecords = append(savedRecords, savedEvents...)
}
return err, savedRecords
return savedRecords, nil
}
func parseEventForOneSemester(url string) ([]model.Event, error) {
@@ -81,25 +84,18 @@ func parseEventForOneSemester(url string) ([]model.Event, error) {
}
// Parse HTML to Node Tree
doc, err2 := parseHTML(err, webpage)
if err2 != nil {
return nil, err2
var doc *html.Node
doc, err = parseHTML(webpage, err)
if err != nil {
return nil, err
}
// Get all event tables and all day labels
eventTables := getEventTables(doc)
allDayLabels := getAllDayLabels(doc)
if eventTables == nil || allDayLabels == nil {
return nil, err
}
eventsWithCombinedWeeks := toEvents(eventTables, allDayLabels)
if eventsWithCombinedWeeks == nil {
return nil, err
}
splitEventsByWeekVal := splitEventsByWeek(eventsWithCombinedWeeks)
events := splitEventsBySingleWeek(splitEventsByWeekVal)
@@ -108,6 +104,11 @@ func parseEventForOneSemester(url string) ([]model.Event, error) {
}
table := findFirstTable(doc)
if table == nil {
return nil, fmt.Errorf("failed to find first table")
}
semesterString := findFirstSpanWithClass(table, "header-0-2-0").FirstChild.Data
semester, year := extractSemesterAndYear(semesterString)
events = convertWeeksToDates(events, semester, year)
@@ -131,7 +132,7 @@ func switchNameAndNotesForExam(events []model.Event) []model.Event {
return events
}
func parseHTML(err error, webpage string) (*html.Node, error) {
func parseHTML(webpage string, err error) (*html.Node, error) {
doc, err := html.Parse(strings.NewReader(webpage))
if err != nil {
return nil, err

View File

@@ -5,23 +5,21 @@ import (
"encoding/json"
"htwkalender/model"
"htwkalender/service/db"
"net/http"
"time"
"github.com/jordic/goics"
"github.com/labstack/echo/v5"
"github.com/pocketbase/pocketbase"
"github.com/pocketbase/pocketbase/apis"
)
const expirationTime = 5 * time.Minute
func Feed(c echo.Context, app *pocketbase.PocketBase, token string) error {
func Feed(app *pocketbase.PocketBase, token string) (string, error) {
var result string
var responseWriter = c.Response().Writer
feed, err := db.FindFeedByToken(token, app)
if feed == nil && err != nil {
return c.JSON(http.StatusNotFound, err)
return "", err
}
modules := make(map[string]model.FeedCollection)
@@ -34,35 +32,26 @@ func Feed(c echo.Context, app *pocketbase.PocketBase, token string) error {
newFeed, err := createFeedForToken(app, modules)
if err != nil {
return c.JSON(http.StatusInternalServerError, err)
return "", err
}
result = newFeed.Content
responseWriter.Header().Set("Content-type", "text/calendar")
responseWriter.Header().Set("charset", "utf-8")
responseWriter.Header().Set("Content-Disposition", "inline")
responseWriter.Header().Set("filename", "calendar.ics")
writeSuccess(result, responseWriter)
c.Response().Writer = responseWriter
return nil
return result, nil
}
func createFeedForToken(app *pocketbase.PocketBase, modules map[string]model.FeedCollection) (*model.FeedModel, error) {
res := db.GetPlanForModules(app, modules)
res, err := db.GetPlanForModules(app, modules)
if err != nil {
return nil, apis.NewNotFoundError("Could not fetch events", err)
}
b := bytes.Buffer{}
goics.NewICalEncode(&b).Encode(IcalModel{Events: res, Mapping: modules})
feed := &model.FeedModel{Content: b.String(), ExpiresAt: time.Now().Add(expirationTime)}
return feed, nil
}
func writeSuccess(message string, w http.ResponseWriter) {
w.WriteHeader(http.StatusOK)
_, err := w.Write([]byte(message))
if err != nil {
return
}
}
func CreateIndividualFeed(requestBody []byte, app *pocketbase.PocketBase) (string, error) {
var modules []model.FeedCollection

View File

@@ -1,32 +1,41 @@
package room
import (
"github.com/pocketbase/pocketbase"
"htwkalender/model"
"htwkalender/service/db"
"net/http"
"github.com/labstack/echo/v5"
"github.com/pocketbase/pocketbase"
)
func GetRooms(c echo.Context, app *pocketbase.PocketBase) error {
rooms := db.GetRooms(app)
return c.JSON(http.StatusOK, rooms)
func GetRooms(app *pocketbase.PocketBase) ([]string, error) {
rooms, err := db.GetRooms(app)
if err != nil {
return nil, err
} else {
return rooms, nil
}
}
func GetRoomScheduleForDay(c echo.Context, app *pocketbase.PocketBase, room string, date string) error {
events := db.GetRoomScheduleForDay(app, room, date)
return c.JSON(http.StatusOK, anonymizeRooms(events))
func GetRoomScheduleForDay(app *pocketbase.PocketBase, room string, date string) ([]model.AnonymizedEventDTO, error) {
roomSchedule, err := db.GetRoomScheduleForDay(app, room, date)
if err != nil {
return nil, err
}
anonymizedRoomSchedule := anonymizeRooms(roomSchedule)
return anonymizedRoomSchedule, nil
}
func GetRoomSchedule(c echo.Context, app *pocketbase.PocketBase, room string, from string, to string) error {
events := db.GetRoomSchedule(app, room, from, to)
return c.JSON(http.StatusOK, anonymizeRooms(events))
func GetRoomSchedule(app *pocketbase.PocketBase, room string, from string, to string) ([]model.AnonymizedEventDTO, error) {
roomSchedule, err := db.GetRoomSchedule(app, room, from, to)
if err != nil {
return nil, err
}
anonymizedRoomSchedule := anonymizeRooms(roomSchedule)
return anonymizedRoomSchedule, nil
}
// Transform the events to anonymized events throwing away all unnecessary information
func anonymizeRooms(events []model.Event) []model.AnonymizedEventDTO {
var anonymizedEvents = []model.AnonymizedEventDTO{}
var anonymizedEvents []model.AnonymizedEventDTO
for _, event := range events {
anonymizedEvents = append(anonymizedEvents, event.AnonymizeEvent())
}

View File

@@ -55,7 +55,7 @@ func Test_anonymizeRooms(t *testing.T) {
args: args{
events: []model.Event{},
},
want: []model.AnonymizedEventDTO{},
want: nil,
},
{
name: "anonymize multiple events",

File diff suppressed because it is too large Load Diff

6
package-lock.json generated
View File

@@ -1,6 +0,0 @@
{
"name": "htwkalender",
"lockfileVersion": 3,
"requires": true,
"packages": {}
}