diff --git a/backend/migrations/1698671745_updated_events.go b/backend/migrations/1698671745_updated_events.go new file mode 100644 index 0000000..6622766 --- /dev/null +++ b/backend/migrations/1698671745_updated_events.go @@ -0,0 +1,53 @@ +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/schema" +) + +func init() { + m.Register(func(db dbx.Builder) error { + dao := daos.New(db) + + collection, err := dao.FindCollectionByNameOrId("7her4515qsmrxe8") + if err != nil { + return err + } + + // add + new_Compulsory := &schema.SchemaField{} + json.Unmarshal([]byte(`{ + "system": false, + "id": "skr0nsbz", + "name": "Compulsory", + "type": "text", + "required": false, + "presentable": false, + "unique": false, + "options": { + "min": null, + "max": null, + "pattern": "" + } + }`), new_Compulsory) + collection.Schema.AddField(new_Compulsory) + + return dao.SaveCollection(collection) + }, func(db dbx.Builder) error { + dao := daos.New(db) + + collection, err := dao.FindCollectionByNameOrId("7her4515qsmrxe8") + if err != nil { + return err + } + + // remove + collection.Schema.RemoveField("skr0nsbz") + + return dao.SaveCollection(collection) + }) +} diff --git a/backend/migrations/1698671759_collections_snapshot.go b/backend/migrations/1698671759_collections_snapshot.go new file mode 100644 index 0000000..678ab38 --- /dev/null +++ b/backend/migrations/1698671759_collections_snapshot.go @@ -0,0 +1,431 @@ +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": "2023-10-30 13:14:28.637Z", + "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": false, + "presentable": false, + "unique": false, + "options": { + "min": null, + "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": "" + } + } + ], + "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": "2023-10-30 13:14:28.637Z", + "name": "feeds", + "type": "base", + "system": false, + "schema": [ + { + "system": false, + "id": "cowxjfmc", + "name": "modules", + "type": "json", + "required": true, + "presentable": false, + "unique": false, + "options": {} + } + ], + "indexes": [], + "listRule": null, + "viewRule": "", + "createRule": null, + "updateRule": "", + "deleteRule": null, + "options": {} + }, + { + "id": "7her4515qsmrxe8", + "created": "2023-09-19 17:31:15.958Z", + "updated": "2023-10-30 13:15:45.797Z", + "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": "tvxitgwc", + "name": "start", + "type": "date", + "required": false, + "presentable": false, + "unique": false, + "options": { + "min": "", + "max": "" + } + }, + { + "system": false, + "id": "trbmsfcz", + "name": "end", + "type": "date", + "required": false, + "presentable": false, + "unique": false, + "options": { + "min": "", + "max": "" + } + }, + { + "system": false, + "id": "skr0nsbz", + "name": "Compulsory", + "type": "text", + "required": false, + "presentable": false, + "unique": false, + "options": { + "min": null, + "max": null, + "pattern": "" + } + } + ], + "indexes": [ + "CREATE UNIQUE INDEX ` + "`" + `idx_orp1NWL` + "`" + ` ON ` + "`" + `events` + "`" + ` (\n ` + "`" + `Day` + "`" + `,\n ` + "`" + `Week` + "`" + `,\n ` + "`" + `Start` + "`" + `,\n ` + "`" + `End` + "`" + `,\n ` + "`" + `Name` + "`" + `,\n ` + "`" + `course` + "`" + `,\n ` + "`" + `Prof` + "`" + `,\n ` + "`" + `Rooms` + "`" + `,\n ` + "`" + `EventType` + "`" + `\n)" + ], + "listRule": null, + "viewRule": null, + "createRule": null, + "updateRule": null, + "deleteRule": null, + "options": {} + }, + { + "id": "_pb_users_auth_", + "created": "2023-10-30 13:14:28.250Z", + "updated": "2023-10-30 13:14:28.637Z", + "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": { + "maxSelect": 1, + "maxSize": 5242880, + "mimeTypes": [ + "image/jpeg", + "image/png", + "image/svg+xml", + "image/gif", + "image/webp" + ], + "thumbs": null, + "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, + "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/model/eventModel.go b/backend/model/eventModel.go index ae2314e..355e594 100644 --- a/backend/model/eventModel.go +++ b/backend/model/eventModel.go @@ -21,6 +21,7 @@ type Event struct { End types.DateTime `db:"end" json:"end"` Name string `db:"Name" json:"name"` EventType string `db:"EventType" json:"eventType"` + Compulsory string `db:"Compulsory" json:"compulsory"` Prof string `db:"Prof" json:"prof"` Rooms string `db:"Rooms" json:"rooms"` Notes string `db:"Notes" json:"notes"` diff --git a/backend/service/db/dbEvents.go b/backend/service/db/dbEvents.go index 5358051..acad6af 100644 --- a/backend/service/db/dbEvents.go +++ b/backend/service/db/dbEvents.go @@ -1,9 +1,10 @@ package db import ( + "htwkalender/model" + "github.com/pocketbase/dbx" "github.com/pocketbase/pocketbase" - "htwkalender/model" ) func SaveEvents(seminarGroup []model.SeminarGroup, app *pocketbase.PocketBase) ([]model.Event, error) { diff --git a/backend/service/fetch/fetchSeminarEventService.go b/backend/service/fetch/fetchSeminarEventService.go index 0c8e677..81521ae 100644 --- a/backend/service/fetch/fetchSeminarEventService.go +++ b/backend/service/fetch/fetchSeminarEventService.go @@ -79,6 +79,21 @@ func GetSeminarGroupsEventsFromHTML(seminarGroupsLabel []string) []model.Seminar return seminarGroups } +func splitEventType(events []model.Event) []model.Event { + + for i, event := range events { + matched, _ := regexp.Match("^(V|P|S)(w|p)$", []byte(event.EventType)) + if matched { + eventType := event.EventType + event.EventType = eventType[0:1] + event.Compulsory = eventType[1:2] + events[i] = event + } + } + + return events +} + func parseSeminarGroup(result string) model.SeminarGroup { doc, err := html.Parse(strings.NewReader(result)) if err != nil { @@ -102,6 +117,7 @@ func parseSeminarGroup(result string) model.SeminarGroup { semester, year := extractSemesterAndYear(semesterString) events = convertWeeksToDates(events, semester, year) events = generateUUIDs(events, course) + events = splitEventType(events) var seminarGroup = model.SeminarGroup{ University: findFirstSpanWithClass(table, "header-1-0-0").FirstChild.Data, Course: course, diff --git a/backend/service/fetch/fetchSeminarEventService_test.go b/backend/service/fetch/fetchSeminarEventService_test.go index 64aed6c..dde66b4 100644 --- a/backend/service/fetch/fetchSeminarEventService_test.go +++ b/backend/service/fetch/fetchSeminarEventService_test.go @@ -120,6 +120,96 @@ func Test_replaceEmptyEventNames(t *testing.T) { } } +func Test_splitEventType(t *testing.T) { + type args struct { + events []model.Event + } + tests := []struct { + name string + args args + want []model.Event + }{ + { + name: "Test 1", + args: args{ + events: []model.Event{ + { + EventType: "V", + }, + }, + }, + want: []model.Event{ + { + EventType: "V", + Compulsory: "", + }, + }, + }, + { + name: "Test 2", + args: args{ + events: []model.Event{ + { + EventType: "Vw", + }, + }, + }, + want: []model.Event{ + { + EventType: "V", + Compulsory: "w", + }, + }, + }, + { + name: "Test 3", + args: args{ + events: []model.Event{ + { + EventType: "Sperr", + }, + }, + }, + want: []model.Event{ + { + EventType: "Sperr", + Compulsory: "", + }, + }, + }, + { + name: "Test 4", + args: args{ + events: []model.Event{ + { + EventType: "Sperr", + }, + { + EventType: "Vw", + }, + }, + }, + want: []model.Event{ + { + EventType: "Sperr", + Compulsory: "", + }, + { + EventType: "V", + Compulsory: "w", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := splitEventType(tt.args.events); !reflect.DeepEqual(got, tt.want) { + t.Errorf("splitEventType() = %v, want %v", got, tt.want) + } + }) + } +} + func Test_generateUUIDs(t *testing.T) { type args struct { events []model.Event diff --git a/backend/service/ical/icalFileGeneration.go b/backend/service/ical/icalFileGeneration.go index 9e9d59c..eb4e2e7 100644 --- a/backend/service/ical/icalFileGeneration.go +++ b/backend/service/ical/icalFileGeneration.go @@ -3,6 +3,7 @@ package ical import ( "htwkalender/model" "htwkalender/service/functions" + "htwkalender/service/names" "time" "github.com/jordic/goics" @@ -32,7 +33,7 @@ func (icalModel IcalModel) EmitICal() goics.Componenter { s.AddProperty(k, v) k, v = goics.FormatDateTime("DTSTART;TZID=Europe/Berlin", event.Start.Time().Local().In(europeTime)) s.AddProperty(k, v) - s.AddProperty("SUMMARY", replaceNameIfUserDefined(event.Name, icalModel.Mapping)) + s.AddProperty("SUMMARY", replaceNameIfUserDefined(&event, icalModel.Mapping)) s.AddProperty("DESCRIPTION", generateDescription(event)) s.AddProperty("LOCATION", event.Rooms) c.AddComponent(s) @@ -40,13 +41,13 @@ func (icalModel IcalModel) EmitICal() goics.Componenter { return c } -func replaceNameIfUserDefined(name string, mapping []model.FeedCollection) string { +func replaceNameIfUserDefined(event *model.Event, mapping []model.FeedCollection) string { for _, mapEntry := range mapping { - if mapEntry.Name == name && !functions.OnlyWhitespace(mapEntry.UserDefinedName) { - return mapEntry.UserDefinedName + if mapEntry.Name == event.Name && !functions.OnlyWhitespace(mapEntry.UserDefinedName) { + return names.ReplaceTemplateSubStrings(mapEntry.UserDefinedName, *event) } } - return name + return event.Name } func generateDescription(event model.Event) string { @@ -62,7 +63,7 @@ func generateDescription(event model.Event) string { description += "Gruppe: " + event.Course + "\n" } if !functions.OnlyWhitespace(event.EventType) { - description += "Typ: " + event.EventType + "\n" + description += "Typ: " + event.EventType + event.Compulsory + "\n" } return description diff --git a/backend/service/names/userDefinedNameTemplates.go b/backend/service/names/userDefinedNameTemplates.go new file mode 100644 index 0000000..489b120 --- /dev/null +++ b/backend/service/names/userDefinedNameTemplates.go @@ -0,0 +1,23 @@ +package names + +import ( + "htwkalender/model" + "regexp" +) + +func ReplaceTemplateSubStrings(rawString string, event model.Event) string { + re := regexp.MustCompile(`\%(.)`) + + return re.ReplaceAllStringFunc(rawString, func(match string) string { + switch match { + case "%%": + return "%" + case "%t": + return event.EventType + case "%p": + return event.Compulsory + default: + return match + } + }) +} diff --git a/backend/service/names/userDefinedNameTemplates_test.go b/backend/service/names/userDefinedNameTemplates_test.go new file mode 100644 index 0000000..7570a8e --- /dev/null +++ b/backend/service/names/userDefinedNameTemplates_test.go @@ -0,0 +1,78 @@ +package names + +import ( + "htwkalender/model" + "testing" +) + +func TestReplaceTemplateSubStrings(t *testing.T) { + type args struct { + rawString string + event model.Event + } + tests := []struct { + name string + args args + want string + }{ + { + name: "Test 1", + args: args{ + rawString: "%t", + event: model.Event{ + EventType: "Test", + }, + }, + want: "Test", + }, + { + name: "Test 2", + args: args{ + rawString: "%p", + event: model.Event{ + Compulsory: "Test", + }, + }, + want: "Test", + }, + { + name: "Test 3", + args: args{ + rawString: "%%", + event: model.Event{ + EventType: "Test", + }, + }, + want: "%", + }, + { + name: "Test 4", + args: args{ + rawString: "%t %p", + event: model.Event{ + EventType: "Test", + Compulsory: "Test", + }, + }, + want: "Test Test", + }, + { + name: "Test 5", + args: args{ + rawString: "%t %p %%", + event: model.Event{ + EventType: "Test", + Compulsory: "Test", + }, + }, + want: "Test Test %", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := ReplaceTemplateSubStrings(tt.args.rawString, tt.args.event); got != tt.want { + t.Errorf("ReplaceTemplateSubStrings() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/frontend/src/components/RenameModules.vue b/frontend/src/components/RenameModules.vue index a43340e..af333bf 100644 --- a/frontend/src/components/RenameModules.vue +++ b/frontend/src/components/RenameModules.vue @@ -3,7 +3,7 @@ import moduleStore from "../store/moduleStore.ts"; import { createIndividualFeed } from "../api/createFeed.ts"; import router from "../router"; import tokenStore from "../store/tokenStore.ts"; -import { ref } from "vue"; +import { Ref, ref } from "vue"; const tableData = ref( moduleStore().modules.map((module) => { @@ -19,6 +19,13 @@ const columns = ref([ { field: "Module", header: "Module" }, ]); +const helpVisible: Ref = ref(false); + +const placeholders = ref([ + { placeholder: "%t", description: "Event Type", examples: "V = Vorlesung, S = Seminar, P = Praktikum/Prüfung" }, + { placeholder: "%p", description: "Mandatory", examples: "w = optional, p = mandatory" }, +]); + async function finalStep() { const token: string = await createIndividualFeed(moduleStore().modules); tokenStore().setToken(token); @@ -30,6 +37,26 @@ async function finalStep() {

Rename your selected Modules to your liking.

+ + +

+ Here you can rename your modules to your liking. This will be the name + of the event in your calendar. +

+

+ You can use the following placeholders in your module names: +

+ + + + + +