diff --git a/backend/service/addSchedule.go b/backend/service/addSchedule.go index b1263c5..56763a7 100644 --- a/backend/service/addSchedule.go +++ b/backend/service/addSchedule.go @@ -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" ) func AddSchedules(app *pocketbase.PocketBase) { @@ -22,17 +26,33 @@ 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 feed.ClearFeeds(app.Dao(), 6, time.RealClock{}) }) - // 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() { + // 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() { 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 }) diff --git a/backend/service/db/dbEvents.go b/backend/service/db/dbEvents.go index e301f00..37ed4cf 100644 --- a/backend/service/db/dbEvents.go +++ b/backend/service/db/dbEvents.go @@ -125,20 +125,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) } diff --git a/backend/service/db/dbEvents_test.go b/backend/service/db/dbEvents_test.go index 8d46131..b8fa224 100644 --- a/backend/service/db/dbEvents_test.go +++ b/backend/service/db/dbEvents_test.go @@ -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 { diff --git a/backend/service/fetch/sport/sportFetcher.go b/backend/service/fetch/sport/sportFetcher.go index 47ba277..1bff08a 100644 --- a/backend/service/fetch/sport/sportFetcher.go +++ b/backend/service/fetch/sport/sportFetcher.go @@ -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, diff --git a/backend/service/fetch/v2/eventParser.go b/backend/service/fetch/v2/eventParser.go index 752bb24..d775603 100644 --- a/backend/service/fetch/v2/eventParser.go +++ b/backend/service/fetch/v2/eventParser.go @@ -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), }) } } diff --git a/backend/service/fetch/v2/fetcher.go b/backend/service/fetch/v2/fetcher.go index 6cafd63..f04d8c5 100644 --- a/backend/service/fetch/v2/fetcher.go +++ b/backend/service/fetch/v2/fetcher.go @@ -30,20 +30,45 @@ func FetchAllEventsAndSave(app *pocketbase.PocketBase) (error, []model.Event) { var savedRecords []model.Event var events []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 (time.Now().Month() >= 3) && (time.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) savedEvents, dbError := db.SaveEvents(events, app) err = dbError - savedRecords = append(savedEvents, events...) + savedRecords = append(savedRecords, savedEvents...) } if (time.Now().Month() >= 9) || (time.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) savedEvents, dbError := db.SaveEvents(events, app) err = dbError - savedRecords = append(savedEvents, events...) + savedRecords = append(savedRecords, savedEvents...) } return err, savedRecords } @@ -89,6 +114,8 @@ func parseEventForOneSemester(url string) ([]model.Event, error) { events = generateUUIDs(events) events = splitEventType(events) + events = switchNameAndNotesForExam(events) + var seminarGroup = model.SeminarGroup{ University: findFirstSpanWithClass(table, "header-1-0-0").FirstChild.Data, Events: events, @@ -101,6 +128,19 @@ func parseEventForOneSemester(url string) ([]model.Event, error) { 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(err error, webpage string) (*html.Node, error) { doc, err := html.Parse(strings.NewReader(webpage)) if err != nil { diff --git a/backend/service/fetch/v2/fetcher_test.go b/backend/service/fetch/v2/fetcher_test.go new file mode 100644 index 0000000..c0acff6 --- /dev/null +++ b/backend/service/fetch/v2/fetcher_test.go @@ -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) + } + }) + } +} diff --git a/frontend/src/api/fetchCourse.ts b/frontend/src/api/fetchCourse.ts index b1e164b..809a2c2 100644 --- a/frontend/src/api/fetchCourse.ts +++ b/frontend/src/api/fetchCourse.ts @@ -36,6 +36,7 @@ export async function fetchModulesByCourseAndSemester( module.uuid, module.name, course, + module.eventType, module.name, module.prof, semester, @@ -61,6 +62,7 @@ export async function fetchAllModules(): Promise { module.uuid, module.name, module.course, + module.eventType, module.name, module.prof, module.semester, diff --git a/frontend/src/api/fetchModule.ts b/frontend/src/api/fetchModule.ts index 85e843d..48da22d 100644 --- a/frontend/src/api/fetchModule.ts +++ b/frontend/src/api/fetchModule.ts @@ -14,6 +14,7 @@ export async function fetchModule(module: Module): Promise { module.uuid, module.name, module.course, + module.eventType, module.name, module.prof, module.semester, diff --git a/frontend/src/components/AdditionalModuleTable.vue b/frontend/src/components/AdditionalModuleTable.vue index 7c0bf0f..3d38780 100644 --- a/frontend/src/components/AdditionalModuleTable.vue +++ b/frontend/src/components/AdditionalModuleTable.vue @@ -33,6 +33,10 @@ const filters = ref({ value: null, matchMode: FilterMatchMode.CONTAINS, }, + eventType: { + value: null, + matchMode: FilterMatchMode.CONTAINS, + }, prof: { value: null, matchMode: FilterMatchMode.CONTAINS, @@ -146,6 +150,21 @@ function unselectModule(event: DataTableRowUnselectEvent) { /> + + +