mirror of
https://gitlab.dit.htwk-leipzig.de/htwk-software/htwkalender.git
synced 2025-08-02 17:59:14 +02:00
Merge branch 'main' into 150-fix-error-response
# Conflicts: # backend/service/addSchedule.go # backend/service/fetch/v2/fetcher.go # frontend/package-lock.json
This commit is contained in:
@@ -5,9 +5,13 @@ import (
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
"github.com/pocketbase/pocketbase/tools/cron"
|
||||
"htwkalender/service/course"
|
||||
"htwkalender/service/events"
|
||||
"htwkalender/service/feed"
|
||||
"htwkalender/service/fetch/sport"
|
||||
v2 "htwkalender/service/fetch/v2"
|
||||
"htwkalender/service/functions/time"
|
||||
"log"
|
||||
"strconv"
|
||||
"log/slog"
|
||||
)
|
||||
|
||||
@@ -24,19 +28,37 @@ func AddSchedules(app *pocketbase.PocketBase) {
|
||||
course.UpdateCourse(app)
|
||||
})
|
||||
|
||||
// Every sunday at 3am clean all courses (5 segments - minute, hour, day, month, weekday) "0 3 * * 0"
|
||||
scheduler.MustAdd("cleanFeeds", "0 3 * * 0", func() {
|
||||
// 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"
|
||||
scheduler.MustAdd("fetchSportEvents", "0 3 * * 0", func() {
|
||||
// Every sunday at 2am fetch all sport events (5 segments - minute, hour, day, month, weekday) "0 2 * * 0"
|
||||
scheduler.MustAdd("fetchEvents", "0 2 * * 0", func() {
|
||||
slog.Info("Started fetching sport events schedule")
|
||||
sport.FetchAndUpdateSportEvents(app)
|
||||
})
|
||||
|
||||
//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)
|
||||
}
|
||||
|
||||
err, savedEvents := v2.FetchAllEventsAndSave(app)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
} else {
|
||||
log.Println("Successfully saved: " + strconv.FormatInt(int64(len(savedEvents)), 10) + " events")
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
scheduler.Start()
|
||||
return nil
|
||||
})
|
||||
|
@@ -7,6 +7,8 @@ import (
|
||||
)
|
||||
|
||||
func Test_getDateFromWeekNumber(t *testing.T) {
|
||||
europeTime, _ := time.LoadLocation("Europe/Berlin")
|
||||
|
||||
type args struct {
|
||||
year int
|
||||
weekNumber int
|
||||
@@ -25,7 +27,7 @@ func Test_getDateFromWeekNumber(t *testing.T) {
|
||||
weekNumber: 1,
|
||||
dayName: "Montag",
|
||||
},
|
||||
want: time.Date(2021, 1, 4, 0, 0, 0, 0, time.UTC),
|
||||
want: time.Date(2021, 1, 4, 0, 0, 0, 0, europeTime),
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
@@ -35,7 +37,7 @@ func Test_getDateFromWeekNumber(t *testing.T) {
|
||||
weekNumber: 57,
|
||||
dayName: "Montag",
|
||||
},
|
||||
want: time.Date(2024, 1, 29, 0, 0, 0, 0, time.UTC),
|
||||
want: time.Date(2024, 1, 29, 0, 0, 0, 0, europeTime),
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
@@ -45,7 +47,7 @@ func Test_getDateFromWeekNumber(t *testing.T) {
|
||||
weekNumber: 1,
|
||||
dayName: "Montag",
|
||||
},
|
||||
want: time.Date(2023, 1, 2, 0, 0, 0, 0, time.UTC),
|
||||
want: time.Date(2023, 1, 2, 0, 0, 0, 0, europeTime),
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
|
@@ -127,20 +127,14 @@ func buildIcalQueryForModules(modules []model.FeedCollection) dbx.Expression {
|
||||
|
||||
//second check if modules has only one element
|
||||
if len(modules) == 1 {
|
||||
return dbx.And(
|
||||
dbx.HashExp{"Name": modules[0].Name},
|
||||
dbx.HashExp{"course": modules[0].Course},
|
||||
)
|
||||
return dbx.HashExp{"uuid": modules[0].UUID}
|
||||
}
|
||||
|
||||
//third check if modules has more than one element
|
||||
var wheres []dbx.Expression
|
||||
|
||||
for _, module := range modules {
|
||||
where := dbx.And(
|
||||
dbx.HashExp{"Name": module.Name},
|
||||
dbx.HashExp{"course": module.Course},
|
||||
)
|
||||
where := dbx.HashExp{"uuid": module.UUID}
|
||||
wheres = append(wheres, where)
|
||||
}
|
||||
|
||||
@@ -196,16 +190,16 @@ func GetAllModulesForCourse(app *pocketbase.PocketBase, course string, semester
|
||||
return events, nil
|
||||
}
|
||||
|
||||
func GetAllModulesDistinctByNameAndCourse(app *pocketbase.PocketBase) (model.Events, error) {
|
||||
var events model.Events
|
||||
func GetAllModulesDistinctByNameAndCourse(app *pocketbase.PocketBase) ([]model.ModuleDTO, error) {
|
||||
var modules []model.ModuleDTO
|
||||
|
||||
err := app.Dao().DB().Select("*").From("events").GroupBy("Name").Distinct(true).All(&events)
|
||||
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)
|
||||
return nil, fmt.Errorf("error while getting events distinct by name and course from data")
|
||||
}
|
||||
|
||||
return events, nil
|
||||
return modules, nil
|
||||
}
|
||||
|
||||
func DeleteAllEventsForCourse(app *pocketbase.PocketBase, course string, semester string) error {
|
||||
|
@@ -23,13 +23,13 @@ func Test_buildIcalQueryForModules(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "one module",
|
||||
args: args{modules: []model.FeedCollection{{Name: "test", Course: "test"}}},
|
||||
want: dbx.And(dbx.HashExp{"Name": "test"}, dbx.HashExp{"course": "test"}),
|
||||
args: args{modules: []model.FeedCollection{{Name: "test", Course: "test", UUID: "test"}}},
|
||||
want: dbx.HashExp{"uuid": "test"},
|
||||
},
|
||||
{
|
||||
name: "two modules",
|
||||
args: args{modules: []model.FeedCollection{{Name: "test", Course: "test"}, {Name: "test2", Course: "test2"}}},
|
||||
want: dbx.Or(dbx.And(dbx.HashExp{"Name": "test"}, dbx.HashExp{"course": "test"}), dbx.And(dbx.HashExp{"Name": "test2"}, dbx.HashExp{"course": "test2"})),
|
||||
args: args{modules: []model.FeedCollection{{Name: "test", Course: "test", UUID: "test"}, {Name: "test2", Course: "test2", UUID: "test2"}}},
|
||||
want: dbx.Or(dbx.HashExp{"uuid": "test"}, dbx.HashExp{"uuid": "test2"}),
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
|
@@ -13,25 +13,47 @@ import (
|
||||
func GetRooms(app *pocketbase.PocketBase) ([]string, error) {
|
||||
|
||||
var events []struct {
|
||||
Rooms string `db:"Rooms" json:"Rooms"`
|
||||
Rooms string `db:"Rooms" json:"Rooms"`
|
||||
Course string `db:"course" json:"Course"`
|
||||
}
|
||||
|
||||
// get all rooms from event records in the events collection
|
||||
err := app.Dao().DB().Select("Rooms").From("events").All(&events)
|
||||
err := app.Dao().DB().Select("Rooms", "course").From("events").Distinct(true).All(&events)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var roomArray []string
|
||||
roomArray := clearAndSeparateRooms([]struct {
|
||||
Rooms string
|
||||
Course string
|
||||
}(events))
|
||||
|
||||
return roomArray
|
||||
}
|
||||
|
||||
func clearAndSeparateRooms(events []struct {
|
||||
Rooms string
|
||||
Course string
|
||||
}) []string {
|
||||
var roomArray []string
|
||||
for _, event := range events {
|
||||
var room = strings.FieldsFunc(event.Rooms, functions.IsSeparator(
|
||||
[]rune{',', ' ', '\t', '\n', '\r', '\u00A0'},
|
||||
))
|
||||
|
||||
var room []string
|
||||
|
||||
// sport rooms don't have to be separated
|
||||
if event.Course != "Sport" {
|
||||
//split rooms by comma, tab, newline, carriage return, semicolon, space and non-breaking space
|
||||
room = strings.FieldsFunc(event.Rooms, functions.IsSeparator(
|
||||
[]rune{',', '\t', '\n', '\r', ';', ' ', '\u00A0'}),
|
||||
)
|
||||
} else {
|
||||
room = append(room, event.Rooms)
|
||||
}
|
||||
|
||||
//split functions room by space and add each room to array if it is not already in there
|
||||
for _, r := range room {
|
||||
var text = strings.TrimSpace(r)
|
||||
if !functions.Contains(roomArray, text) && !strings.Contains(text, " ") && len(text) >= 1 {
|
||||
if !functions.Contains(roomArray, text) && len(text) >= 1 {
|
||||
roomArray = append(roomArray, text)
|
||||
}
|
||||
}
|
||||
|
@@ -12,17 +12,28 @@ import (
|
||||
func GetModulesForCourseDistinct(app *pocketbase.PocketBase, course string, semester string) (model.Events, error) {
|
||||
|
||||
modules, err := db.GetAllModulesForCourse(app, course, semester)
|
||||
replaceEmptyEntry(modules, "Sonderveranstaltungen")
|
||||
|
||||
// Convert the []model.Module to []Named
|
||||
var namedEvents []Named
|
||||
for _, module := range modules {
|
||||
namedEvents = append(namedEvents, &module)
|
||||
}
|
||||
|
||||
replaceEmptyEntry(namedEvents, "Sonderveranstaltungen")
|
||||
return modules, err
|
||||
}
|
||||
|
||||
type Named interface {
|
||||
GetName() string
|
||||
SetName(name string)
|
||||
}
|
||||
|
||||
// replaceEmptyEntry replaces an empty entry in a module with a replacement string
|
||||
// If the module is not empty, nothing happens
|
||||
func replaceEmptyEntry(modules model.Events, replacement string) {
|
||||
|
||||
for i, module := range modules {
|
||||
if functions.OnlyWhitespace(module.Name) {
|
||||
modules[i].Name = replacement
|
||||
func replaceEmptyEntry(namedList []Named, replacement string) {
|
||||
for i, namedItem := range namedList {
|
||||
if functions.OnlyWhitespace(namedItem.GetName()) {
|
||||
namedList[i].SetName(replacement)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -32,7 +43,12 @@ func replaceEmptyEntry(modules model.Events, replacement string) {
|
||||
func GetAllModulesDistinct(app *pocketbase.PocketBase, c echo.Context) error {
|
||||
modules, err := db.GetAllModulesDistinctByNameAndCourse(app)
|
||||
|
||||
replaceEmptyEntry(modules, "Sonderveranstaltungen")
|
||||
var namedModules []Named
|
||||
for _, module := range modules {
|
||||
namedModules = append(namedModules, &module)
|
||||
}
|
||||
|
||||
replaceEmptyEntry(namedModules, "Sonderveranstaltungen")
|
||||
|
||||
if err != nil {
|
||||
return c.JSON(400, err)
|
||||
|
@@ -18,8 +18,9 @@ import (
|
||||
"github.com/PuerkitoBio/goquery"
|
||||
)
|
||||
|
||||
// @TODO: add tests
|
||||
// @TODO: make it like a cron job to fetch the sport courses once a week
|
||||
// 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 {
|
||||
|
||||
var sportCourseLinks = fetchAllAvailableSportCourses()
|
||||
@@ -56,6 +57,7 @@ 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
|
||||
@@ -89,7 +91,7 @@ func formatEntriesToEvents(entries []model.SportEntry) []model.Event {
|
||||
Week: strconv.Itoa(23),
|
||||
Start: start,
|
||||
End: end,
|
||||
Name: entry.Title + " " + entry.Details.Type + " (" + entry.ID + ")",
|
||||
Name: entry.Title + " (" + entry.ID + ")",
|
||||
EventType: entry.Details.Type,
|
||||
Prof: entry.Details.CourseLead.Name,
|
||||
Rooms: entry.Details.Location.Name,
|
||||
|
@@ -267,7 +267,6 @@ func Test_generateUUIDs(t *testing.T) {
|
||||
}
|
||||
|
||||
func Test_createTimeFromHourAndMinuteString(t *testing.T) {
|
||||
europeTime, _ := time.LoadLocation("Europe/Berlin")
|
||||
type args struct {
|
||||
tableTime string
|
||||
}
|
||||
@@ -281,21 +280,21 @@ func Test_createTimeFromHourAndMinuteString(t *testing.T) {
|
||||
args: args{
|
||||
tableTime: "08:00",
|
||||
},
|
||||
want: time.Date(0, 0, 0, 8, 0, 0, 0, europeTime),
|
||||
want: time.Date(0, 0, 0, 8, 0, 0, 0, time.UTC),
|
||||
},
|
||||
{
|
||||
name: "Test 2",
|
||||
args: args{
|
||||
tableTime: "08:15",
|
||||
},
|
||||
want: time.Date(0, 0, 0, 8, 15, 0, 0, europeTime),
|
||||
want: time.Date(0, 0, 0, 8, 15, 0, 0, time.UTC),
|
||||
},
|
||||
{
|
||||
name: "Test 3",
|
||||
args: args{
|
||||
tableTime: "08:30",
|
||||
},
|
||||
want: time.Date(0, 0, 0, 8, 30, 0, 0, europeTime),
|
||||
want: time.Date(0, 0, 0, 8, 30, 0, 0, time.UTC),
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
|
@@ -35,7 +35,7 @@ func toEvents(tables [][]*html.Node, days []string) []model.Event {
|
||||
Prof: getTextContent(tableData[6]),
|
||||
Rooms: getTextContent(tableData[8]),
|
||||
BookedAt: getTextContent(tableData[10]),
|
||||
Course: course,
|
||||
Course: strings.TrimSpace(course),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@@ -23,8 +23,33 @@ func ParseEventsFromRemote(app *pocketbase.PocketBase) (model.Events, error) {
|
||||
func FetchAllEventsAndSave(app *pocketbase.PocketBase, clock localTime.Clock) ([]model.Event, error) {
|
||||
var savedRecords []model.Event
|
||||
|
||||
var stubUrl = [2]string{
|
||||
"https://stundenplan.htwk-leipzig.de/",
|
||||
"/Berichte/Text-Listen;Veranstaltungsarten;name;" +
|
||||
"Vp%0A" +
|
||||
"Vw%0A" +
|
||||
"V%0A" +
|
||||
"Sp%0A" +
|
||||
"Sw%0A" +
|
||||
"S%0A" +
|
||||
"Pp%0A" +
|
||||
"Pw%0A" +
|
||||
"P%0A" +
|
||||
"ZV%0A" +
|
||||
"Tut%0A" +
|
||||
"Sperr%0A" +
|
||||
"pf%0A" +
|
||||
"wpf%0A" +
|
||||
"fak%0A" +
|
||||
"Pruefung%0A" +
|
||||
"Vertretung%0A" +
|
||||
"Fremdveranst.%0A" +
|
||||
"Buchen%0A" +
|
||||
"%0A?&template=sws_modul&weeks=1-65&combined=yes",
|
||||
}
|
||||
|
||||
if (clock.Now().Month() >= 3) && (clock.Now().Month() <= 10) {
|
||||
url := "https://stundenplan.htwk-leipzig.de/ss/Berichte/Text-Listen;Veranstaltungsarten;name;Vp%0AVw%0AV%0ASp%0ASw%0AS%0APp%0APw%0AP%0AZV%0ATut%0ASperr%0Apf%0Awpf%0Afak%0A%0A?&template=sws_modul&weeks=1-65&combined=yes"
|
||||
url := stubUrl[0] + "ss" + stubUrl[1]
|
||||
events, err := parseEventForOneSemester(url)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse events for summmer semester: %w", err)
|
||||
@@ -37,7 +62,7 @@ func FetchAllEventsAndSave(app *pocketbase.PocketBase, clock localTime.Clock) ([
|
||||
}
|
||||
|
||||
if (clock.Now().Month() >= 9) || (clock.Now().Month() <= 4) {
|
||||
url := "https://stundenplan.htwk-leipzig.de/ws/Berichte/Text-Listen;Veranstaltungsarten;name;Vp%0AVw%0AV%0ASp%0ASw%0AS%0APp%0APw%0AP%0AZV%0ATut%0ASperr%0Apf%0Awpf%0Afak%0A%0A?&template=sws_modul&weeks=1-65&combined=yes"
|
||||
url := stubUrl[0] + "ws" + stubUrl[1]
|
||||
events, err := parseEventForOneSemester(url)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse events for winter semester: %w", err)
|
||||
@@ -46,7 +71,7 @@ func FetchAllEventsAndSave(app *pocketbase.PocketBase, clock localTime.Clock) ([
|
||||
if dbError != nil {
|
||||
return nil, fmt.Errorf("failed to save events: %w", dbError)
|
||||
}
|
||||
savedRecords = append(savedEvents, events...)
|
||||
savedRecords = append(savedRecords, savedEvents...)
|
||||
}
|
||||
return savedRecords, nil
|
||||
}
|
||||
@@ -87,21 +112,26 @@ func parseEventForOneSemester(url string) ([]model.Event, error) {
|
||||
semesterString := findFirstSpanWithClass(table, "header-0-2-0").FirstChild.Data
|
||||
semester, year := extractSemesterAndYear(semesterString)
|
||||
events = convertWeeksToDates(events, semester, year)
|
||||
events = generateUUIDs(events)
|
||||
events = splitEventType(events)
|
||||
|
||||
var seminarGroup = model.SeminarGroup{
|
||||
University: findFirstSpanWithClass(table, "header-1-0-0").FirstChild.Data,
|
||||
Events: events,
|
||||
}
|
||||
|
||||
if seminarGroup.Events == nil && seminarGroup.University == "" {
|
||||
return nil, err
|
||||
}
|
||||
events = switchNameAndNotesForExam(events)
|
||||
events = generateUUIDs(events)
|
||||
|
||||
return events, nil
|
||||
}
|
||||
|
||||
// switch name and notes for Pruefung events when Note is not empty and Name starts with "Prüfungen" and contains email
|
||||
func switchNameAndNotesForExam(events []model.Event) []model.Event {
|
||||
for i, event := range events {
|
||||
if event.EventType == "Pruefung" {
|
||||
if event.Notes != "" && strings.HasPrefix(event.Name, "Prüfungen") && strings.Contains(event.Name, "@") {
|
||||
events[i].Name = event.Notes
|
||||
events[i].Notes = event.Name
|
||||
}
|
||||
}
|
||||
}
|
||||
return events
|
||||
}
|
||||
|
||||
func parseHTML(webpage string, err error) (*html.Node, error) {
|
||||
doc, err := html.Parse(strings.NewReader(webpage))
|
||||
if err != nil {
|
||||
|
83
backend/service/fetch/v2/fetcher_test.go
Normal file
83
backend/service/fetch/v2/fetcher_test.go
Normal file
@@ -0,0 +1,83 @@
|
||||
package v2
|
||||
|
||||
import (
|
||||
"htwkalender/model"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func Test_switchNameAndNotesForExam(t *testing.T) {
|
||||
type args struct {
|
||||
events []model.Event
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want []model.Event
|
||||
}{
|
||||
{
|
||||
name: "switch name and notes for exam",
|
||||
args: args{
|
||||
events: []model.Event{
|
||||
{
|
||||
EventType: "Pruefung",
|
||||
Name: "Prüfungen FING/EIT WiSe (pruefungsamt.fing-eit@htwk-leipzig.de)",
|
||||
Notes: "Computer Vision II - Räume/Zeit unter Vorbehalt- (Raum W111.1)",
|
||||
},
|
||||
},
|
||||
},
|
||||
want: []model.Event{
|
||||
{
|
||||
EventType: "Pruefung",
|
||||
Name: "Computer Vision II - Räume/Zeit unter Vorbehalt- (Raum W111.1)",
|
||||
Notes: "Prüfungen FING/EIT WiSe (pruefungsamt.fing-eit@htwk-leipzig.de)",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "dont switch name and notes for exam",
|
||||
args: args{
|
||||
events: []model.Event{
|
||||
{
|
||||
EventType: "Pruefung",
|
||||
Name: "i054 Umweltschutz und Recycling DPB & VNB 7.FS (wpf)",
|
||||
Notes: "Prüfung",
|
||||
},
|
||||
},
|
||||
},
|
||||
want: []model.Event{
|
||||
{
|
||||
EventType: "Pruefung",
|
||||
Notes: "Prüfung",
|
||||
Name: "i054 Umweltschutz und Recycling DPB & VNB 7.FS (wpf)",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "dont switch name and notes for exam",
|
||||
args: args{
|
||||
events: []model.Event{
|
||||
{
|
||||
EventType: "Pruefung",
|
||||
Name: "Prüfungen FING/ME WiSe (pruefungsamt.fing-me@htwk-leipzig.de)",
|
||||
Notes: "",
|
||||
},
|
||||
},
|
||||
},
|
||||
want: []model.Event{
|
||||
{
|
||||
EventType: "Pruefung",
|
||||
Notes: "",
|
||||
Name: "Prüfungen FING/ME WiSe (pruefungsamt.fing-me@htwk-leipzig.de)",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := switchNameAndNotesForExam(tt.args.events); !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("switchNameAndNotesForExam() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user