Merge pull request #116 from HTWK-Leipzig/104-exams-in-calendar

104 exams in calendar
This commit is contained in:
masterElmar
2023-12-29 03:31:18 +01:00
committed by GitHub
13 changed files with 190 additions and 26 deletions

View File

@@ -5,9 +5,13 @@ import (
"github.com/pocketbase/pocketbase/core" "github.com/pocketbase/pocketbase/core"
"github.com/pocketbase/pocketbase/tools/cron" "github.com/pocketbase/pocketbase/tools/cron"
"htwkalender/service/course" "htwkalender/service/course"
"htwkalender/service/events"
"htwkalender/service/feed" "htwkalender/service/feed"
"htwkalender/service/fetch/sport" "htwkalender/service/fetch/sport"
v2 "htwkalender/service/fetch/v2"
"htwkalender/service/functions/time" "htwkalender/service/functions/time"
"log"
"strconv"
) )
func AddSchedules(app *pocketbase.PocketBase) { func AddSchedules(app *pocketbase.PocketBase) {
@@ -22,17 +26,33 @@ func AddSchedules(app *pocketbase.PocketBase) {
course.UpdateCourse(app) course.UpdateCourse(app)
}) })
// Every sunday at 3am clean all courses (5 segments - minute, hour, day, month, weekday) "0 3 * * 0" // Every sunday at 1am clean all courses (5 segments - minute, hour, day, month, weekday) "0 3 * * 0"
scheduler.MustAdd("cleanFeeds", "0 3 * * 0", func() { scheduler.MustAdd("cleanFeeds", "0 1 * * 0", func() {
// clean feeds older than 6 months // clean feeds older than 6 months
feed.ClearFeeds(app.Dao(), 6, time.RealClock{}) 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" // Every sunday at 3am fetch all sport events (5 segments - minute, hour, day, month, weekday) "0 2 * * 0"
scheduler.MustAdd("fetchEvents", "0 2 * * 0", func() { scheduler.MustAdd("fetchSportEvents", "0 3 * * 0", func() {
sport.FetchAndUpdateSportEvents(app) 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() scheduler.Start()
return nil return nil
}) })

View File

@@ -125,20 +125,14 @@ func buildIcalQueryForModules(modules []model.FeedCollection) dbx.Expression {
//second check if modules has only one element //second check if modules has only one element
if len(modules) == 1 { if len(modules) == 1 {
return dbx.And( return dbx.HashExp{"uuid": modules[0].UUID}
dbx.HashExp{"Name": modules[0].Name},
dbx.HashExp{"course": modules[0].Course},
)
} }
//third check if modules has more than one element //third check if modules has more than one element
var wheres []dbx.Expression var wheres []dbx.Expression
for _, module := range modules { for _, module := range modules {
where := dbx.And( where := dbx.HashExp{"uuid": module.UUID}
dbx.HashExp{"Name": module.Name},
dbx.HashExp{"course": module.Course},
)
wheres = append(wheres, where) wheres = append(wheres, where)
} }

View File

@@ -23,13 +23,13 @@ func Test_buildIcalQueryForModules(t *testing.T) {
}, },
{ {
name: "one module", name: "one module",
args: args{modules: []model.FeedCollection{{Name: "test", Course: "test"}}}, args: args{modules: []model.FeedCollection{{Name: "test", Course: "test", UUID: "test"}}},
want: dbx.And(dbx.HashExp{"Name": "test"}, dbx.HashExp{"course": "test"}), want: dbx.HashExp{"uuid": "test"},
}, },
{ {
name: "two modules", name: "two modules",
args: args{modules: []model.FeedCollection{{Name: "test", Course: "test"}, {Name: "test2", Course: "test2"}}}, args: args{modules: []model.FeedCollection{{Name: "test", Course: "test", UUID: "test"}, {Name: "test2", Course: "test2", UUID: "test2"}}},
want: dbx.Or(dbx.And(dbx.HashExp{"Name": "test"}, dbx.HashExp{"course": "test"}), dbx.And(dbx.HashExp{"Name": "test2"}, dbx.HashExp{"course": "test2"})), want: dbx.Or(dbx.HashExp{"uuid": "test"}, dbx.HashExp{"uuid": "test2"}),
}, },
} }
for _, tt := range tests { for _, tt := range tests {

View File

@@ -18,8 +18,9 @@ import (
"github.com/PuerkitoBio/goquery" "github.com/PuerkitoBio/goquery"
) )
// @TODO: add tests // FetchAndUpdateSportEvents fetches all sport events from the HTWK sport website
// @TODO: make it like a cron job to fetch the sport courses once a week // 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 {
var sportCourseLinks = fetchAllAvailableSportCourses() 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()) err = db.DeleteAllEventsForCourse(app, "Sport", functions.GetCurrentSemesterString())
if err != nil { if err != nil {
return nil return nil
@@ -89,7 +91,7 @@ func formatEntriesToEvents(entries []model.SportEntry) []model.Event {
Week: strconv.Itoa(23), Week: strconv.Itoa(23),
Start: start, Start: start,
End: end, End: end,
Name: entry.Title + " " + entry.Details.Type + " (" + entry.ID + ")", Name: entry.Title + " (" + entry.ID + ")",
EventType: entry.Details.Type, EventType: entry.Details.Type,
Prof: entry.Details.CourseLead.Name, Prof: entry.Details.CourseLead.Name,
Rooms: entry.Details.Location.Name, Rooms: entry.Details.Location.Name,

View File

@@ -35,7 +35,7 @@ func toEvents(tables [][]*html.Node, days []string) []model.Event {
Prof: getTextContent(tableData[6]), Prof: getTextContent(tableData[6]),
Rooms: getTextContent(tableData[8]), Rooms: getTextContent(tableData[8]),
BookedAt: getTextContent(tableData[10]), BookedAt: getTextContent(tableData[10]),
Course: course, Course: strings.TrimSpace(course),
}) })
} }
} }

View File

@@ -30,20 +30,45 @@ func FetchAllEventsAndSave(app *pocketbase.PocketBase) (error, []model.Event) {
var savedRecords []model.Event var savedRecords []model.Event
var events []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) { 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) events, err = parseEventForOneSemester(url)
savedEvents, dbError := db.SaveEvents(events, app) savedEvents, dbError := db.SaveEvents(events, app)
err = dbError err = dbError
savedRecords = append(savedEvents, events...) savedRecords = append(savedRecords, savedEvents...)
} }
if (time.Now().Month() >= 9) || (time.Now().Month() <= 4) { 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) events, err = parseEventForOneSemester(url)
savedEvents, dbError := db.SaveEvents(events, app) savedEvents, dbError := db.SaveEvents(events, app)
err = dbError err = dbError
savedRecords = append(savedEvents, events...) savedRecords = append(savedRecords, savedEvents...)
} }
return err, savedRecords return err, savedRecords
} }
@@ -89,6 +114,8 @@ func parseEventForOneSemester(url string) ([]model.Event, error) {
events = generateUUIDs(events) events = generateUUIDs(events)
events = splitEventType(events) events = splitEventType(events)
events = switchNameAndNotesForExam(events)
var seminarGroup = model.SeminarGroup{ var seminarGroup = model.SeminarGroup{
University: findFirstSpanWithClass(table, "header-1-0-0").FirstChild.Data, University: findFirstSpanWithClass(table, "header-1-0-0").FirstChild.Data,
Events: events, Events: events,
@@ -101,6 +128,19 @@ func parseEventForOneSemester(url string) ([]model.Event, error) {
return events, nil 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) { func parseHTML(err error, webpage string) (*html.Node, error) {
doc, err := html.Parse(strings.NewReader(webpage)) doc, err := html.Parse(strings.NewReader(webpage))
if err != nil { if err != nil {

View 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)
}
})
}
}

View File

@@ -36,6 +36,7 @@ export async function fetchModulesByCourseAndSemester(
module.uuid, module.uuid,
module.name, module.name,
course, course,
module.eventType,
module.name, module.name,
module.prof, module.prof,
semester, semester,
@@ -61,6 +62,7 @@ export async function fetchAllModules(): Promise<Module[]> {
module.uuid, module.uuid,
module.name, module.name,
module.course, module.course,
module.eventType,
module.name, module.name,
module.prof, module.prof,
module.semester, module.semester,

View File

@@ -14,6 +14,7 @@ export async function fetchModule(module: Module): Promise<Module> {
module.uuid, module.uuid,
module.name, module.name,
module.course, module.course,
module.eventType,
module.name, module.name,
module.prof, module.prof,
module.semester, module.semester,

View File

@@ -33,6 +33,10 @@ const filters = ref({
value: null, value: null,
matchMode: FilterMatchMode.CONTAINS, matchMode: FilterMatchMode.CONTAINS,
}, },
eventType: {
value: null,
matchMode: FilterMatchMode.CONTAINS,
},
prof: { prof: {
value: null, value: null,
matchMode: FilterMatchMode.CONTAINS, matchMode: FilterMatchMode.CONTAINS,
@@ -146,6 +150,21 @@ function unselectModule(event: DataTableRowUnselectEvent) {
/> />
</template> </template>
</Column> </Column>
<Column
field="eventType"
:header="$t('additionalModules.eventType')"
:show-clear-button="false"
:show-filter-menu="false"
>
<template #filter="{ filterModel, filterCallback }">
<InputText
v-model="filterModel.value"
type="text"
class="p-column-filter max-w-10rem"
@input="filterCallback()"
/>
</template>
</Column>
<Column <Column
field="prof" field="prof"
:header="$t('additionalModules.professor')" :header="$t('additionalModules.professor')"

View File

@@ -93,7 +93,8 @@
"from": "", "from": "",
"to": " bis ", "to": " bis ",
"of": " von insgesamt " "of": " von insgesamt "
} },
"eventType": "Ereignistyp"
}, },
"renameModules": { "renameModules": {
"reminder": "Erinnerung", "reminder": "Erinnerung",

View File

@@ -93,7 +93,8 @@
"from": "", "from": "",
"to": " to ", "to": " to ",
"of": " of " "of": " of "
} },
"eventType": "event type"
}, },
"renameModules": { "renameModules": {
"reminder": "reminder", "reminder": "reminder",

View File

@@ -5,6 +5,7 @@ export class Module {
public uuid: string, public uuid: string,
public name: string, public name: string,
public course: string, public course: string,
public eventType: string,
public userDefinedName: string, public userDefinedName: string,
public prof: string, public prof: string,
public semester: string, public semester: string,