diff --git a/backend/migrations/1706827339_collections_snapshot.go b/backend/migrations/1706827339_collections_snapshot.go new file mode 100644 index 0000000..5680481 --- /dev/null +++ b/backend/migrations/1706827339_collections_snapshot.go @@ -0,0 +1,461 @@ +package migrations + +import ( + "encoding/json" + + "github.com/pocketbase/dbx" + "github.com/pocketbase/pocketbase/daos" + m "github.com/pocketbase/pocketbase/migrations" + "github.com/pocketbase/pocketbase/models" +) + +func init() { + m.Register(func(db dbx.Builder) error { + jsonData := `[ + { + "id": "cfq9mqlmd97v8z5", + "created": "2023-09-19 17:31:15.957Z", + "updated": "2024-02-01 22:35:50.512Z", + "name": "groups", + "type": "base", + "system": false, + "schema": [ + { + "system": false, + "id": "85msl21p", + "name": "university", + "type": "text", + "required": false, + "presentable": false, + "unique": false, + "options": { + "min": null, + "max": null, + "pattern": "" + } + }, + { + "system": false, + "id": "2sii4dtp", + "name": "shortcut", + "type": "text", + "required": false, + "presentable": false, + "unique": false, + "options": { + "min": null, + "max": null, + "pattern": "" + } + }, + { + "system": false, + "id": "uiwgo28f", + "name": "groupId", + "type": "text", + "required": false, + "presentable": false, + "unique": false, + "options": { + "min": null, + "max": null, + "pattern": "" + } + }, + { + "system": false, + "id": "y0l1lrzs", + "name": "course", + "type": "text", + "required": true, + "presentable": false, + "unique": false, + "options": { + "min": 2, + "max": null, + "pattern": "" + } + }, + { + "system": false, + "id": "kr62mhbz", + "name": "faculty", + "type": "text", + "required": false, + "presentable": false, + "unique": false, + "options": { + "min": null, + "max": null, + "pattern": "" + } + }, + { + "system": false, + "id": "ya6znpez", + "name": "facultyId", + "type": "text", + "required": false, + "presentable": false, + "unique": false, + "options": { + "min": null, + "max": null, + "pattern": "" + } + }, + { + "system": false, + "id": "bdhcrksy", + "name": "semester", + "type": "text", + "required": true, + "presentable": false, + "unique": false, + "options": { + "min": 2, + "max": 2, + "pattern": "ws|ss" + } + } + ], + "indexes": [ + "CREATE UNIQUE INDEX ` + "`" + `idx_rcaN2Oq` + "`" + ` ON ` + "`" + `groups` + "`" + ` (` + "`" + `course` + "`" + `)" + ], + "listRule": null, + "viewRule": null, + "createRule": null, + "updateRule": null, + "deleteRule": null, + "options": {} + }, + { + "id": "d65h4wh7zk13gxp", + "created": "2023-09-19 17:31:15.957Z", + "updated": "2024-02-01 13:34:43.834Z", + "name": "feeds", + "type": "base", + "system": false, + "schema": [ + { + "system": false, + "id": "cowxjfmc", + "name": "modules", + "type": "json", + "required": true, + "presentable": false, + "unique": false, + "options": { + "maxSize": 2000000 + } + }, + { + "system": false, + "id": "wmmney8x", + "name": "retrieved", + "type": "date", + "required": false, + "presentable": false, + "unique": false, + "options": { + "min": "", + "max": "" + } + } + ], + "indexes": [], + "listRule": null, + "viewRule": "", + "createRule": null, + "updateRule": "", + "deleteRule": null, + "options": {} + }, + { + "id": "7her4515qsmrxe8", + "created": "2023-09-19 17:31:15.958Z", + "updated": "2024-02-01 13:34:43.833Z", + "name": "events", + "type": "base", + "system": false, + "schema": [ + { + "system": false, + "id": "m8ne8e3m", + "name": "Day", + "type": "text", + "required": false, + "presentable": false, + "unique": false, + "options": { + "min": null, + "max": null, + "pattern": "" + } + }, + { + "system": false, + "id": "xnsxqp7j", + "name": "Week", + "type": "text", + "required": false, + "presentable": false, + "unique": false, + "options": { + "min": null, + "max": null, + "pattern": "" + } + }, + { + "system": false, + "id": "aeuskrjo", + "name": "Name", + "type": "text", + "required": false, + "presentable": false, + "unique": false, + "options": { + "min": null, + "max": null, + "pattern": "" + } + }, + { + "system": false, + "id": "klrzqyw0", + "name": "EventType", + "type": "text", + "required": false, + "presentable": false, + "unique": false, + "options": { + "min": null, + "max": null, + "pattern": "" + } + }, + { + "system": false, + "id": "5zltexoy", + "name": "Prof", + "type": "text", + "required": false, + "presentable": false, + "unique": false, + "options": { + "min": null, + "max": null, + "pattern": "" + } + }, + { + "system": false, + "id": "gy3nvfmx", + "name": "Rooms", + "type": "text", + "required": false, + "presentable": false, + "unique": false, + "options": { + "min": null, + "max": null, + "pattern": "" + } + }, + { + "system": false, + "id": "hn7b8dfy", + "name": "Notes", + "type": "text", + "required": false, + "presentable": false, + "unique": false, + "options": { + "min": null, + "max": null, + "pattern": "" + } + }, + { + "system": false, + "id": "axskpwm8", + "name": "BookedAt", + "type": "text", + "required": false, + "presentable": false, + "unique": false, + "options": { + "min": null, + "max": null, + "pattern": "" + } + }, + { + "system": false, + "id": "vyyefxp7", + "name": "course", + "type": "text", + "required": false, + "presentable": false, + "unique": false, + "options": { + "min": null, + "max": null, + "pattern": "" + } + }, + { + "system": false, + "id": "vlbpm9fz", + "name": "semester", + "type": "text", + "required": false, + "presentable": false, + "unique": false, + "options": { + "min": null, + "max": null, + "pattern": "" + } + }, + { + "system": false, + "id": "0kahthzr", + "name": "uuid", + "type": "text", + "required": false, + "presentable": false, + "unique": false, + "options": { + "min": null, + "max": null, + "pattern": "" + } + }, + { + "system": false, + "id": "6hkjwgb4", + "name": "start", + "type": "date", + "required": false, + "presentable": false, + "unique": false, + "options": { + "min": "", + "max": "" + } + }, + { + "system": false, + "id": "szbefpjf", + "name": "end", + "type": "date", + "required": false, + "presentable": false, + "unique": false, + "options": { + "min": "", + "max": "" + } + }, + { + "system": false, + "id": "nlnnxu7x", + "name": "Compulsory", + "type": "text", + "required": false, + "presentable": false, + "unique": false, + "options": { + "min": null, + "max": null, + "pattern": "" + } + } + ], + "indexes": [ + "CREATE INDEX ` + "`" + `idx_4vOTAiC` + "`" + ` ON ` + "`" + `events` + "`" + ` (\n ` + "`" + `Name` + "`" + `,\n ` + "`" + `course` + "`" + `,\n ` + "`" + `start` + "`" + `,\n ` + "`" + `end` + "`" + `,\n ` + "`" + `semester` + "`" + `,\n ` + "`" + `EventType` + "`" + `,\n ` + "`" + `Compulsory` + "`" + `\n)" + ], + "listRule": null, + "viewRule": null, + "createRule": null, + "updateRule": null, + "deleteRule": null, + "options": {} + }, + { + "id": "_pb_users_auth_", + "created": "2024-02-01 13:34:43.663Z", + "updated": "2024-02-01 13:34:43.833Z", + "name": "users", + "type": "auth", + "system": false, + "schema": [ + { + "system": false, + "id": "users_name", + "name": "name", + "type": "text", + "required": false, + "presentable": false, + "unique": false, + "options": { + "min": null, + "max": null, + "pattern": "" + } + }, + { + "system": false, + "id": "users_avatar", + "name": "avatar", + "type": "file", + "required": false, + "presentable": false, + "unique": false, + "options": { + "mimeTypes": [ + "image/jpeg", + "image/png", + "image/svg+xml", + "image/gif", + "image/webp" + ], + "thumbs": null, + "maxSelect": 1, + "maxSize": 5242880, + "protected": false + } + } + ], + "indexes": [], + "listRule": "id = @request.auth.id", + "viewRule": "id = @request.auth.id", + "createRule": "", + "updateRule": "id = @request.auth.id", + "deleteRule": "id = @request.auth.id", + "options": { + "allowEmailAuth": true, + "allowOAuth2Auth": true, + "allowUsernameAuth": true, + "exceptEmailDomains": null, + "manageRule": null, + "minPasswordLength": 8, + "onlyEmailDomains": null, + "onlyVerified": false, + "requireEmail": false + } + } + ]` + + collections := []*models.Collection{} + if err := json.Unmarshal([]byte(jsonData), &collections); err != nil { + return err + } + + return daos.New(db).ImportCollections(collections, true, nil) + }, func(db dbx.Builder) error { + return nil + }) +} diff --git a/backend/migrations/1706827586_updated_groups.go b/backend/migrations/1706827586_updated_groups.go new file mode 100644 index 0000000..1ee5286 --- /dev/null +++ b/backend/migrations/1706827586_updated_groups.go @@ -0,0 +1,39 @@ +package migrations + +import ( + "encoding/json" + + "github.com/pocketbase/dbx" + "github.com/pocketbase/pocketbase/daos" + m "github.com/pocketbase/pocketbase/migrations" +) + +func init() { + m.Register(func(db dbx.Builder) error { + dao := daos.New(db) + + collection, err := dao.FindCollectionByNameOrId("cfq9mqlmd97v8z5") + if err != nil { + return err + } + + json.Unmarshal([]byte(`[ + "CREATE UNIQUE INDEX `+"`"+`idx_rcaN2Oq`+"`"+` ON `+"`"+`groups`+"`"+` (\n `+"`"+`course`+"`"+`,\n `+"`"+`semester`+"`"+`\n)" + ]`), &collection.Indexes) + + return dao.SaveCollection(collection) + }, func(db dbx.Builder) error { + dao := daos.New(db) + + collection, err := dao.FindCollectionByNameOrId("cfq9mqlmd97v8z5") + if err != nil { + return err + } + + json.Unmarshal([]byte(`[ + "CREATE UNIQUE INDEX `+"`"+`idx_rcaN2Oq`+"`"+` ON `+"`"+`groups`+"`"+` (`+"`"+`course`+"`"+`)" + ]`), &collection.Indexes) + + return dao.SaveCollection(collection) + }) +} diff --git a/backend/model/seminarGroup.go b/backend/model/seminarGroup.go index d3d63c9..72ac424 100644 --- a/backend/model/seminarGroup.go +++ b/backend/model/seminarGroup.go @@ -7,5 +7,6 @@ type SeminarGroup struct { Course string Faculty string FacultyId string + Semester string Events []Event } diff --git a/backend/model/seminarGroupXMLStruct.go b/backend/model/seminarGroupXMLStruct.go index e1bb502..c8a5626 100644 --- a/backend/model/seminarGroupXMLStruct.go +++ b/backend/model/seminarGroupXMLStruct.go @@ -5,11 +5,11 @@ import ( ) type Studium struct { - XMLName xml.Name `xml:"studium"` - Fakultaet []Fakultaet `xml:"fakultaet"` + XMLName xml.Name `xml:"studium"` + Faculty []Faculty `xml:"fakultaet"` } -type Fakultaet struct { +type Faculty struct { XMLName xml.Name `xml:"fakultaet"` Name string `xml:"name,attr"` ID string `xml:"id,attr"` diff --git a/backend/service/addRoute.go b/backend/service/addRoute.go index 0eeb892..a9fa997 100644 --- a/backend/service/addRoute.go +++ b/backend/service/addRoute.go @@ -307,6 +307,25 @@ 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/courses", + Handler: func(c echo.Context) error { + semester := c.QueryParam("semester") + courses := events.GetAllCoursesForSemester(app, semester) + return c.JSON(200, courses) + }, + 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, diff --git a/backend/service/db/dbGroups.go b/backend/service/db/dbGroups.go index 40965e4..545b071 100644 --- a/backend/service/db/dbGroups.go +++ b/backend/service/db/dbGroups.go @@ -14,7 +14,7 @@ func SaveGroups(seminarGroup []model.SeminarGroup, collection *models.Collection var insertRecords []*models.Record for _, group := range seminarGroup { - dbGroup, err := FindGroupByCourse(group.Course, app) + dbGroup, err := FindGroupByCourseAndSemester(group.Course, group.Semester, app) if dbGroup == nil && err.Error() == "sql: no rows in result set" { tobeSavedGroups = append(tobeSavedGroups, group) @@ -32,6 +32,7 @@ func SaveGroups(seminarGroup []model.SeminarGroup, collection *models.Collection record.Set("course", group.Course) record.Set("faculty", group.Faculty) record.Set("facultyId", group.FacultyId) + record.Set("semester", group.Semester) insertRecords = append(insertRecords, record) } @@ -50,13 +51,13 @@ func SaveGroups(seminarGroup []model.SeminarGroup, collection *models.Collection return savedRecords, nil } -func FindGroupByCourse(course string, app *pocketbase.PocketBase) (*model.SeminarGroup, error) { +func FindGroupByCourseAndSemester(course string, semester string, app *pocketbase.PocketBase) (*model.SeminarGroup, error) { var group model.SeminarGroup - err := app.Dao().DB().Select("*").From("groups").Where(dbx.NewExp("course = {:course}", dbx.Params{"course": course})).One(&group) + err := app.Dao().DB().Select("*").From("groups").Where(dbx.NewExp("course = {:course} AND semester = {:semester}", dbx.Params{"course": course, "semester": semester})).One(&group) if err != nil { return nil, err } - return &group, err + return &group, nil } func GetAllCourses(app *pocketbase.PocketBase) []string { @@ -80,3 +81,26 @@ func GetAllCourses(app *pocketbase.PocketBase) []string { return courseArray } + +func GetAllCoursesForSemester(app *pocketbase.PocketBase, semester string) interface{} { + + var courses []struct { + CourseShortcut string `db:"course" json:"course"` + } + + // get all rooms from event records in the events collection + err := app.Dao().DB().Select("course").From("groups").Where(dbx.NewExp("semester = {:semester}", dbx.Params{"semester": semester})).All(&courses) + if err != nil { + slog.Error("Error while getting groups from database: ", err) + return []string{} + } + + var courseArray []string + + for _, course := range courses { + courseArray = append(courseArray, course.CourseShortcut) + } + + return courseArray + +} diff --git a/backend/service/events/courseService.go b/backend/service/events/courseService.go index 8388925..3f9e99c 100644 --- a/backend/service/events/courseService.go +++ b/backend/service/events/courseService.go @@ -8,3 +8,7 @@ import ( func GetAllCourses(app *pocketbase.PocketBase) []string { return db.GetAllCourses(app) } + +func GetAllCoursesForSemester(app *pocketbase.PocketBase, semester string) interface{} { + return db.GetAllCoursesForSemester(app, semester) +} diff --git a/backend/service/fetch/v1/fetchSeminarEventService.go b/backend/service/fetch/v1/fetchSeminarEventService.go index eace23c..6228ca6 100644 --- a/backend/service/fetch/v1/fetchSeminarEventService.go +++ b/backend/service/fetch/v1/fetchSeminarEventService.go @@ -8,6 +8,7 @@ import ( "htwkalender/model" "htwkalender/service/date" "htwkalender/service/fetch" + "htwkalender/service/functions" "regexp" "strconv" "strings" @@ -17,7 +18,7 @@ import ( func ReplaceEmptyEventNames(groups []model.SeminarGroup) []model.SeminarGroup { for i, group := range groups { for j, event := range group.Events { - if event.Name == "" { + if functions.OnlyWhitespace(event.Name) { groups[i].Events[j].Name = "Sonderveranstaltungen" } } diff --git a/backend/service/fetch/v1/fetchSeminarGroupService.go b/backend/service/fetch/v1/fetchSeminarGroupService.go index 2baf0f9..f4c87b8 100644 --- a/backend/service/fetch/v1/fetchSeminarGroupService.go +++ b/backend/service/fetch/v1/fetchSeminarGroupService.go @@ -57,8 +57,8 @@ func FetchSeminarGroups(app *pocketbase.PocketBase) ([]*models.Record, error) { return nil, err } - groups = parseSeminarGroups(resultSummer) - groups = append(groups, parseSeminarGroups(resultWinter)...) + groups = parseSeminarGroups(resultSummer, "ss") + groups = append(groups, parseSeminarGroups(resultWinter, "ws")...) // filter duplicates groups = removeDuplicates(groups) @@ -91,14 +91,14 @@ func removeDuplicates(groups []model.SeminarGroup) []model.SeminarGroup { func contains(groups []model.SeminarGroup, group model.SeminarGroup) bool { for _, a := range groups { - if a.Course == group.Course { + if (a.Course == group.Course) && (a.Semester == group.Semester) { return true } } return false } -func parseSeminarGroups(result string) []model.SeminarGroup { +func parseSeminarGroups(result string, semester string) []model.SeminarGroup { var studium model.Studium err := xml.Unmarshal([]byte(result), &studium) @@ -107,16 +107,17 @@ func parseSeminarGroups(result string) []model.SeminarGroup { } var seminarGroups []model.SeminarGroup - for _, Fakultaet := range studium.Fakultaet { - for _, Studiengang := range Fakultaet.Studiengang { + for _, faculty := range studium.Faculty { + for _, Studiengang := range faculty.Studiengang { for _, Studienrichtung := range Studiengang.Semgrp { seminarGroup := model.SeminarGroup{ University: "HTWK-Leipzig", GroupShortcut: Studiengang.Name, GroupId: Studiengang.ID, Course: Studienrichtung.Name, - Faculty: Fakultaet.Name, - FacultyId: Fakultaet.ID, + Faculty: faculty.Name, + FacultyId: faculty.ID, + Semester: semester, } seminarGroups = append(seminarGroups, seminarGroup) } diff --git a/backend/service/fetch/v1/fetchSeminarGroupService_test.go b/backend/service/fetch/v1/fetchSeminarGroupService_test.go new file mode 100644 index 0000000..58e4dd5 --- /dev/null +++ b/backend/service/fetch/v1/fetchSeminarGroupService_test.go @@ -0,0 +1,75 @@ +package v1 + +import ( + "htwkalender/model" + "testing" +) + +func Test_contains(t *testing.T) { + type args struct { + groups []model.SeminarGroup + group model.SeminarGroup + } + tests := []struct { + name string + args args + want bool + }{ + { + name: "should return true if group is in groups", + args: args{ + groups: []model.SeminarGroup{ + { + Course: "test", + Semester: "test", + }, + }, + group: model.SeminarGroup{ + Course: "test", + Semester: "test", + }, + }, + want: true, + }, + { + name: "should return false if group is not in groups", + args: args{ + groups: []model.SeminarGroup{ + { + Course: "test", + Semester: "test", + }, + }, + group: model.SeminarGroup{ + Course: "test", + Semester: "test2", + }, + }, + want: false, + }, + { + name: "should return false if group is not in courses", + args: args{ + groups: []model.SeminarGroup{ + { + Course: "test3", + Semester: "test", + }, + }, + group: model.SeminarGroup{ + Course: "test", + Semester: "test", + }, + }, + want: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := contains(tt.args.groups, tt.args.group); got != tt.want { + t.Errorf("contains() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/backend/service/fetch/v2/eventParser.go b/backend/service/fetch/v2/eventParser.go index d775603..9824fc8 100644 --- a/backend/service/fetch/v2/eventParser.go +++ b/backend/service/fetch/v2/eventParser.go @@ -5,6 +5,7 @@ import ( "golang.org/x/net/html" "htwkalender/model" "htwkalender/service/date" + "htwkalender/service/functions" "regexp" "strings" ) @@ -21,6 +22,10 @@ func toEvents(tables [][]*html.Node, days []string) []model.Event { end, _ := types.ParseDateTime(date.CreateTimeFromHourAndMinuteString(getTextContent(tableData[2]))) courses := getTextContent(tableData[7]) + name := getTextContent(tableData[3]) + if functions.OnlyWhitespace(name) { + name = "Sonderveranstaltung" + } if len(courses) > 0 { for _, course := range strings.Split(courses, " ") { @@ -29,7 +34,7 @@ func toEvents(tables [][]*html.Node, days []string) []model.Event { Week: getTextContent(tableData[0]), Start: start, End: end, - Name: getTextContent(tableData[3]), + Name: name, EventType: getTextContent(tableData[4]), Notes: getTextContent(tableData[5]), Prof: getTextContent(tableData[6]), diff --git a/frontend/src/api/fetchCourse.ts b/frontend/src/api/fetchCourse.ts index 809a2c2..e953c7c 100644 --- a/frontend/src/api/fetchCourse.ts +++ b/frontend/src/api/fetchCourse.ts @@ -20,6 +20,26 @@ export async function fetchCourse(): Promise { return courses; } +export async function fetchCourseBySemester( + semester: string, +): Promise { + const courses: string[] = []; + await fetch("/api/courses?semester=" + semester) + .then((response) => { + //check if response type is json + const contentType = response.headers.get("content-type"); + if (contentType && contentType.indexOf("application/json") !== -1) { + return response.json(); + } else { + return []; + } + }) + .then((coursesResponse) => { + coursesResponse.forEach((course: string) => courses.push(course)); + }); + return courses; +} + export async function fetchModulesByCourseAndSemester( course: string, semester: string, diff --git a/frontend/src/components/CalendarLink.vue b/frontend/src/components/CalendarLink.vue index 0d928b8..a64a2bf 100644 --- a/frontend/src/components/CalendarLink.vue +++ b/frontend/src/components/CalendarLink.vue @@ -91,4 +91,10 @@ const actions = computed(() => [ - + diff --git a/frontend/src/components/ModuleInformation.vue b/frontend/src/components/ModuleInformation.vue index 5dd6ab7..920224e 100644 --- a/frontend/src/components/ModuleInformation.vue +++ b/frontend/src/components/ModuleInformation.vue @@ -81,6 +81,10 @@ function formatWeekday(weekday: string) { field="eventType" :header="$t('moduleInformation.type')" > + {{ item.rooms }} + + + {{ $t("moduleInformation.notes") }}: + + {{ item.notes }} + diff --git a/frontend/src/components/RenameModules.vue b/frontend/src/components/RenameModules.vue index e6343b9..df6361b 100644 --- a/frontend/src/components/RenameModules.vue +++ b/frontend/src/components/RenameModules.vue @@ -93,14 +93,16 @@ async function finalStep() { }}