mirror of
https://gitlab.dit.htwk-leipzig.de/htwk-software/htwkalender.git
synced 2025-08-03 18:29:14 +02:00
Merge branch 'main' into 15-calendar-preview
# Conflicts: # frontend/src/view/AdditionalModules.vue # frontend/src/view/EditCalendarView.vue
This commit is contained in:
@@ -35,6 +35,14 @@ htwkalender-demo
|
|||||||
|
|
||||||
Execute the following api calls to fetch data manually from HTWK and store it in the database:
|
Execute the following api calls to fetch data manually from HTWK and store it in the database:
|
||||||
|
|
||||||
|
Both api calls need a token to be executed.
|
||||||
|
You can get a token by logging in to the admin ui and copy the token from the local storage.
|
||||||
|
Add this attribute to the request header of the api call:
|
||||||
|
|
||||||
|
```
|
||||||
|
Authorization : eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3MDE3MDA3ODQsImlkIjoicnA0Ym54YXNyczM5emR4IiwidHlwZSI6ImFkbWluIn0.j7Bt3-uaZ8CoNt8D9Oxjk7ZwvHDGZJy1xe3aq4BID3w
|
||||||
|
```
|
||||||
|
|
||||||
The first command will fetch all groups and store them in the database.
|
The first command will fetch all groups and store them in the database.
|
||||||
This should be done quick in a few seconds (0-5s).
|
This should be done quick in a few seconds (0-5s).
|
||||||
When you execute the command again, it will update the groups in the
|
When you execute the command again, it will update the groups in the
|
||||||
|
52
backend/migrations/1700512738_updated_feeds.go
Normal file
52
backend/migrations/1700512738_updated_feeds.go
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
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("d65h4wh7zk13gxp")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// add
|
||||||
|
new_retrieved := &schema.SchemaField{}
|
||||||
|
json.Unmarshal([]byte(`{
|
||||||
|
"system": false,
|
||||||
|
"id": "wmmney8x",
|
||||||
|
"name": "retrieved",
|
||||||
|
"type": "date",
|
||||||
|
"required": false,
|
||||||
|
"presentable": false,
|
||||||
|
"unique": false,
|
||||||
|
"options": {
|
||||||
|
"min": "",
|
||||||
|
"max": ""
|
||||||
|
}
|
||||||
|
}`), new_retrieved)
|
||||||
|
collection.Schema.AddField(new_retrieved)
|
||||||
|
|
||||||
|
return dao.SaveCollection(collection)
|
||||||
|
}, func(db dbx.Builder) error {
|
||||||
|
dao := daos.New(db)
|
||||||
|
|
||||||
|
collection, err := dao.FindCollectionByNameOrId("d65h4wh7zk13gxp")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// remove
|
||||||
|
collection.Schema.RemoveField("wmmney8x")
|
||||||
|
|
||||||
|
return dao.SaveCollection(collection)
|
||||||
|
})
|
||||||
|
}
|
444
backend/migrations/1700512916_collections_snapshot.go
Normal file
444
backend/migrations/1700512916_collections_snapshot.go
Normal file
@@ -0,0 +1,444 @@
|
|||||||
|
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-11-01 21:17:43.567Z",
|
||||||
|
"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-11-20 20:38:58.258Z",
|
||||||
|
"name": "feeds",
|
||||||
|
"type": "base",
|
||||||
|
"system": false,
|
||||||
|
"schema": [
|
||||||
|
{
|
||||||
|
"system": false,
|
||||||
|
"id": "cowxjfmc",
|
||||||
|
"name": "modules",
|
||||||
|
"type": "json",
|
||||||
|
"required": true,
|
||||||
|
"presentable": false,
|
||||||
|
"unique": false,
|
||||||
|
"options": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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": "2023-11-01 21:17:43.567Z",
|
||||||
|
"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": "2023-11-01 21:17:43.390Z",
|
||||||
|
"updated": "2023-11-01 21:17:43.567Z",
|
||||||
|
"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
|
||||||
|
})
|
||||||
|
}
|
@@ -1,9 +1,13 @@
|
|||||||
package model
|
package model
|
||||||
|
|
||||||
import "github.com/pocketbase/pocketbase/models"
|
import (
|
||||||
|
"github.com/pocketbase/pocketbase/models"
|
||||||
|
"github.com/pocketbase/pocketbase/tools/types"
|
||||||
|
)
|
||||||
|
|
||||||
type Feed struct {
|
type Feed struct {
|
||||||
Modules string `db:"modules" json:"modules"`
|
Modules string `db:"modules" json:"modules"`
|
||||||
|
Retrieved types.DateTime `db:"retrieved" json:"retrieved"`
|
||||||
models.BaseModel
|
models.BaseModel
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -1,10 +1,10 @@
|
|||||||
package model
|
package model
|
||||||
|
|
||||||
type Module struct {
|
type Module struct {
|
||||||
UUID string `json:"uuid"`
|
UUID string `json:"uuid" db:"uuid"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name" db:"Name"`
|
||||||
Prof string `json:"prof"`
|
Prof string `json:"prof" db:"Prof"`
|
||||||
Course string `json:"course"`
|
Course string `json:"course" db:"course"`
|
||||||
Semester string `json:"semester"`
|
Semester string `json:"semester" db:"semester"`
|
||||||
Events Events `json:"events"`
|
Events Events `json:"events"`
|
||||||
}
|
}
|
||||||
|
@@ -11,12 +11,24 @@ paths:
|
|||||||
/api/fetchPlans:
|
/api/fetchPlans:
|
||||||
get:
|
get:
|
||||||
summary: Fetch Seminar Plans
|
summary: Fetch Seminar Plans
|
||||||
|
security:
|
||||||
|
- ApiKeyAuth: []
|
||||||
responses:
|
responses:
|
||||||
'200':
|
'200':
|
||||||
description: Successful response
|
description: Successful response
|
||||||
/api/fetchGroups:
|
/api/fetchGroups:
|
||||||
get:
|
get:
|
||||||
summary: Fetch Seminar Groups
|
summary: Fetch Seminar Groups
|
||||||
|
security:
|
||||||
|
- ApiKeyAuth: []
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: Successful response
|
||||||
|
/api/modules:
|
||||||
|
delete:
|
||||||
|
summary: Delete Module
|
||||||
|
security:
|
||||||
|
- ApiKeyAuth: []
|
||||||
responses:
|
responses:
|
||||||
'200':
|
'200':
|
||||||
description: Successful response
|
description: Successful response
|
||||||
@@ -26,15 +38,123 @@ paths:
|
|||||||
responses:
|
responses:
|
||||||
'200':
|
'200':
|
||||||
description: Successful response
|
description: Successful response
|
||||||
/api/feedURL:
|
/api/schedule/day:
|
||||||
get:
|
get:
|
||||||
summary: Get iCal Feed URL
|
summary: Get Day Schedule
|
||||||
|
parameters:
|
||||||
|
- name: room
|
||||||
|
in: query
|
||||||
|
description: room
|
||||||
|
example: "LN006-H"
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
- name: date
|
||||||
|
in: query
|
||||||
|
description: date
|
||||||
|
example: "2023-11-26"
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: Successful response
|
||||||
|
/api/schedule:
|
||||||
|
get:
|
||||||
|
summary: Get Schedule
|
||||||
|
parameters:
|
||||||
|
- name: room
|
||||||
|
in: query
|
||||||
|
description: room
|
||||||
|
example: "LN006-H"
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
- name: from
|
||||||
|
in: query
|
||||||
|
description: date
|
||||||
|
example: "2023-11-26"
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
- name: to
|
||||||
|
in: query
|
||||||
|
description: date
|
||||||
|
example: "2023-11-30"
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: Successful response
|
||||||
|
/api/createFeed:
|
||||||
|
post:
|
||||||
|
summary: Create iCal Feed
|
||||||
|
requestBody:
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: '#/components/schemas/Module'
|
||||||
|
required:
|
||||||
|
- name
|
||||||
|
- modules
|
||||||
responses:
|
responses:
|
||||||
'200':
|
'200':
|
||||||
description: Successful response
|
description: Successful response
|
||||||
/api/feed:
|
/api/feed:
|
||||||
get:
|
get:
|
||||||
summary: Get iCal Feed for calendar
|
summary: Get iCal Feed for calendar
|
||||||
|
parameters:
|
||||||
|
- name: token
|
||||||
|
in: query
|
||||||
|
description: calendar token
|
||||||
|
required: true
|
||||||
|
example: "ldluwzg3e73ffxq"
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: Successful response
|
||||||
|
/api/course/modules:
|
||||||
|
get:
|
||||||
|
summary: Get Modules for Course
|
||||||
|
parameters:
|
||||||
|
- name: course
|
||||||
|
in: query
|
||||||
|
description: course
|
||||||
|
required: true
|
||||||
|
example: "Software Engineering"
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
- name: semester
|
||||||
|
in: query
|
||||||
|
description: semester
|
||||||
|
required: true
|
||||||
|
example: "ws"
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: Successful response
|
||||||
|
/api/module:
|
||||||
|
get:
|
||||||
|
summary: Get Module
|
||||||
|
parameters:
|
||||||
|
- name: uuid
|
||||||
|
in: query
|
||||||
|
description: uuid
|
||||||
|
required: true
|
||||||
|
example: "d0b3a0e0-2f1a-4e1a-8b0a-0b9e1a0b9e1a"
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: Successful response
|
||||||
|
/api/courses:
|
||||||
|
get:
|
||||||
|
summary: Get Courses
|
||||||
responses:
|
responses:
|
||||||
'200':
|
'200':
|
||||||
description: Successful response
|
description: Successful response
|
||||||
@@ -51,3 +171,52 @@ paths:
|
|||||||
responses:
|
responses:
|
||||||
'200':
|
'200':
|
||||||
description: Successful response
|
description: Successful response
|
||||||
|
/api/events:
|
||||||
|
delete:
|
||||||
|
summary: Delete Event
|
||||||
|
security:
|
||||||
|
- ApiKeyAuth: []
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: Successful response
|
||||||
|
/api/feeds/migrate:
|
||||||
|
get:
|
||||||
|
summary: Migrates all iCal Feeds in the database to the new format
|
||||||
|
security:
|
||||||
|
- ApiKeyAuth: []
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: Successful response
|
||||||
|
security:
|
||||||
|
- ApiKeyAuth: []
|
||||||
|
components:
|
||||||
|
securitySchemes:
|
||||||
|
ApiKeyAuth:
|
||||||
|
type: apiKey
|
||||||
|
in: header
|
||||||
|
name: Authorization
|
||||||
|
schemas:
|
||||||
|
Module:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
name:
|
||||||
|
type: string
|
||||||
|
description: name
|
||||||
|
example: "Software Engineering"
|
||||||
|
uuid:
|
||||||
|
type: string
|
||||||
|
format: uuid
|
||||||
|
course:
|
||||||
|
type: string
|
||||||
|
userDefinedName:
|
||||||
|
type: string
|
||||||
|
prof:
|
||||||
|
type: string
|
||||||
|
semester:
|
||||||
|
type: string
|
||||||
|
reminder:
|
||||||
|
type: boolean
|
||||||
|
events:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: string
|
@@ -1,7 +1,6 @@
|
|||||||
package service
|
package service
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"htwkalender/model"
|
|
||||||
"htwkalender/service/events"
|
"htwkalender/service/events"
|
||||||
"htwkalender/service/fetch"
|
"htwkalender/service/fetch"
|
||||||
"htwkalender/service/ical"
|
"htwkalender/service/ical"
|
||||||
@@ -26,6 +25,7 @@ func AddRoutes(app *pocketbase.PocketBase) {
|
|||||||
},
|
},
|
||||||
Middlewares: []echo.MiddlewareFunc{
|
Middlewares: []echo.MiddlewareFunc{
|
||||||
apis.ActivityLogger(app),
|
apis.ActivityLogger(app),
|
||||||
|
apis.RequireAdminAuth(),
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -61,6 +61,7 @@ func AddRoutes(app *pocketbase.PocketBase) {
|
|||||||
},
|
},
|
||||||
Middlewares: []echo.MiddlewareFunc{
|
Middlewares: []echo.MiddlewareFunc{
|
||||||
apis.ActivityLogger(app),
|
apis.ActivityLogger(app),
|
||||||
|
apis.RequireAdminAuth(),
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -212,18 +213,11 @@ func AddRoutes(app *pocketbase.PocketBase) {
|
|||||||
|
|
||||||
app.OnBeforeServe().Add(func(e *core.ServeEvent) error {
|
app.OnBeforeServe().Add(func(e *core.ServeEvent) error {
|
||||||
_, err := e.Router.AddRoute(echo.Route{
|
_, err := e.Router.AddRoute(echo.Route{
|
||||||
Method: http.MethodPost,
|
Method: http.MethodGet,
|
||||||
Path: "/api/module",
|
Path: "/api/module",
|
||||||
Handler: func(c echo.Context) error {
|
Handler: func(c echo.Context) error {
|
||||||
|
requestModule := c.QueryParam("uuid")
|
||||||
var requestModule model.Module
|
module, err := events.GetModuleByUUID(app, requestModule)
|
||||||
|
|
||||||
if err := c.Bind(&requestModule); err != nil {
|
|
||||||
return apis.NewBadRequestError("Failed to read request body", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
module, err := events.GetModuleByName(app, requestModule)
|
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return c.JSON(400, err)
|
return c.JSON(400, err)
|
||||||
} else {
|
} else {
|
||||||
@@ -286,7 +280,7 @@ func AddRoutes(app *pocketbase.PocketBase) {
|
|||||||
app.OnBeforeServe().Add(func(e *core.ServeEvent) error {
|
app.OnBeforeServe().Add(func(e *core.ServeEvent) error {
|
||||||
_, err := e.Router.AddRoute(echo.Route{
|
_, err := e.Router.AddRoute(echo.Route{
|
||||||
Method: http.MethodGet,
|
Method: http.MethodGet,
|
||||||
Path: "/api/feed/migrate",
|
Path: "/api/feeds/migrate",
|
||||||
Handler: func(c echo.Context) error {
|
Handler: func(c echo.Context) error {
|
||||||
err := ical.MigrateFeedJson(app)
|
err := ical.MigrateFeedJson(app)
|
||||||
|
|
||||||
|
@@ -4,8 +4,9 @@ import (
|
|||||||
"github.com/pocketbase/pocketbase"
|
"github.com/pocketbase/pocketbase"
|
||||||
"github.com/pocketbase/pocketbase/core"
|
"github.com/pocketbase/pocketbase/core"
|
||||||
"github.com/pocketbase/pocketbase/tools/cron"
|
"github.com/pocketbase/pocketbase/tools/cron"
|
||||||
"htwkalender/service/events"
|
"htwkalender/service/course"
|
||||||
"log"
|
"htwkalender/service/feed"
|
||||||
|
"htwkalender/service/functions/time"
|
||||||
)
|
)
|
||||||
|
|
||||||
func AddSchedules(app *pocketbase.PocketBase) {
|
func AddSchedules(app *pocketbase.PocketBase) {
|
||||||
@@ -17,23 +18,15 @@ func AddSchedules(app *pocketbase.PocketBase) {
|
|||||||
// Every three hours update all courses (5 segments - minute, hour, day, month, weekday) "0 */3 * * *"
|
// Every three hours update all courses (5 segments - minute, hour, day, month, weekday) "0 */3 * * *"
|
||||||
// Every 10 minutes update all courses (5 segments - minute, hour, day, month, weekday) "*/10 * * * *"
|
// Every 10 minutes update all courses (5 segments - minute, hour, day, month, weekday) "*/10 * * * *"
|
||||||
scheduler.MustAdd("updateCourse", "0 */3 * * *", func() {
|
scheduler.MustAdd("updateCourse", "0 */3 * * *", func() {
|
||||||
|
course.UpdateCourse(app)
|
||||||
courses := events.GetAllCourses(app)
|
|
||||||
|
|
||||||
for _, course := range courses {
|
|
||||||
err := events.UpdateModulesForCourse(app, course)
|
|
||||||
if err != nil {
|
|
||||||
log.Println("Update Course: " + course + " failed")
|
|
||||||
log.Println(err)
|
|
||||||
} else {
|
|
||||||
log.Println("Update Course: " + course + " successful")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Every sunday at 3am clean all courses (5 segments - minute, hour, day, month, weekday) "0 3 * * 0"
|
||||||
|
scheduler.MustAdd("cleanFeeds", "0 3 * * 0", func() {
|
||||||
|
// clean feeds older than 6 months
|
||||||
|
feed.ClearFeeds(app.Dao(), 6, time.RealClock{})
|
||||||
|
})
|
||||||
scheduler.Start()
|
scheduler.Start()
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|
||||||
|
20
backend/service/course/courseFunctions.go
Normal file
20
backend/service/course/courseFunctions.go
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
package course
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/pocketbase/pocketbase"
|
||||||
|
"htwkalender/service/events"
|
||||||
|
"log"
|
||||||
|
)
|
||||||
|
|
||||||
|
func UpdateCourse(app *pocketbase.PocketBase) {
|
||||||
|
courses := events.GetAllCourses(app)
|
||||||
|
for _, course := range courses {
|
||||||
|
err := events.UpdateModulesForCourse(app, course)
|
||||||
|
if err != nil {
|
||||||
|
log.Println("Update Course: " + course + " failed")
|
||||||
|
log.Println(err)
|
||||||
|
} else {
|
||||||
|
log.Println("Update Course: " + course + " successful")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -196,6 +196,18 @@ func DeleteAllEvents(app *pocketbase.PocketBase) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func FindModuleByUUID(app *pocketbase.PocketBase, uuid string) (model.Module, error) {
|
||||||
|
var module model.Module
|
||||||
|
|
||||||
|
err := app.Dao().DB().Select("*").From("events").Where(dbx.NewExp("uuid = {:uuid}", dbx.Params{"uuid": uuid})).One(&module)
|
||||||
|
if err != nil {
|
||||||
|
print("Error while getting events from database: ", err)
|
||||||
|
return model.Module{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return module, nil
|
||||||
|
}
|
||||||
|
|
||||||
func FindAllEventsByModule(app *pocketbase.PocketBase, module model.Module) (model.Events, error) {
|
func FindAllEventsByModule(app *pocketbase.PocketBase, module model.Module) (model.Events, error) {
|
||||||
var events model.Events
|
var events model.Events
|
||||||
|
|
||||||
|
@@ -1,10 +1,11 @@
|
|||||||
package db
|
package db
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/pocketbase/dbx"
|
|
||||||
"github.com/pocketbase/pocketbase"
|
"github.com/pocketbase/pocketbase"
|
||||||
|
"github.com/pocketbase/pocketbase/daos"
|
||||||
"github.com/pocketbase/pocketbase/models"
|
"github.com/pocketbase/pocketbase/models"
|
||||||
"htwkalender/model"
|
"htwkalender/model"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
func SaveFeed(feed model.Feed, collection *models.Collection, app *pocketbase.PocketBase) (*models.Record, error) {
|
func SaveFeed(feed model.Feed, collection *models.Collection, app *pocketbase.PocketBase) (*models.Record, error) {
|
||||||
@@ -19,10 +20,30 @@ func SaveFeed(feed model.Feed, collection *models.Collection, app *pocketbase.Po
|
|||||||
}
|
}
|
||||||
|
|
||||||
func FindFeedByToken(token string, app *pocketbase.PocketBase) (*model.Feed, error) {
|
func FindFeedByToken(token string, app *pocketbase.PocketBase) (*model.Feed, error) {
|
||||||
var feed model.Feed
|
|
||||||
err := app.Dao().DB().Select("*").From("feeds").Where(dbx.NewExp("id = {:id}", dbx.Params{"id": token})).One(&feed)
|
record, err := app.Dao().FindRecordById("feeds", token)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var feed model.Feed
|
||||||
|
feed.Modules = record.GetString("modules")
|
||||||
|
feed.Retrieved = record.GetDateTime("retrieved")
|
||||||
|
|
||||||
|
//update retrieved time
|
||||||
|
record.Set("retrieved", time.Now())
|
||||||
|
|
||||||
|
err = app.Dao().SaveRecord(record)
|
||||||
|
|
||||||
return &feed, err
|
return &feed, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func GetAllFeeds(db *daos.Dao) ([]model.Feed, error) {
|
||||||
|
var feeds []model.Feed
|
||||||
|
err := db.DB().Select("*").From("feeds").All(&feeds)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return feeds, nil
|
||||||
|
}
|
||||||
|
@@ -42,11 +42,8 @@ func GetAllModulesDistinct(app *pocketbase.PocketBase, c echo.Context) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetModuleByName returns a module by its name
|
func GetModuleByUUID(app *pocketbase.PocketBase, uuid string) (model.Module, error) {
|
||||||
// If the module does not exist, an error is returned
|
module, err := db.FindModuleByUUID(app, uuid)
|
||||||
// If the module exists, the module is returned
|
|
||||||
// Module is a struct that exists in database as events
|
|
||||||
func GetModuleByName(app *pocketbase.PocketBase, module model.Module) (model.Module, error) {
|
|
||||||
events, err := db.FindAllEventsByModule(app, module)
|
events, err := db.FindAllEventsByModule(app, module)
|
||||||
|
|
||||||
if err != nil || len(events) == 0 {
|
if err != nil || len(events) == 0 {
|
||||||
|
35
backend/service/feed/feedFunctions.go
Normal file
35
backend/service/feed/feedFunctions.go
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
package feed
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/pocketbase/dbx"
|
||||||
|
"github.com/pocketbase/pocketbase/daos"
|
||||||
|
database "htwkalender/service/db"
|
||||||
|
localTime "htwkalender/service/functions/time"
|
||||||
|
"log"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ClearFeeds(db *daos.Dao, months int, clock localTime.Clock) {
|
||||||
|
feeds, err := database.GetAllFeeds(db)
|
||||||
|
if err != nil {
|
||||||
|
log.Println("CleanFeeds: get all feeds failed")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for _, feed := range feeds {
|
||||||
|
// if retrieved time is older than a half year delete feed
|
||||||
|
now := clock.Now()
|
||||||
|
feedRetrievedTime := feed.Retrieved.Time()
|
||||||
|
timeShift := now.AddDate(0, -months, 0)
|
||||||
|
|
||||||
|
if feedRetrievedTime.Before(timeShift) {
|
||||||
|
// delete feed
|
||||||
|
sqlResult, err := db.DB().Delete("feeds", dbx.NewExp("id = {:id}", dbx.Params{"id": feed.GetId()})).Execute()
|
||||||
|
if err != nil {
|
||||||
|
log.Println("CleanFeeds: delete feed " + feed.GetId() + " failed")
|
||||||
|
log.Println(err)
|
||||||
|
log.Println(sqlResult)
|
||||||
|
} else {
|
||||||
|
log.Println("CleanFeeds: delete feed " + feed.GetId() + " successful")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
83
backend/service/feed/feedFunctions_test.go
Normal file
83
backend/service/feed/feedFunctions_test.go
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
package feed
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/pocketbase/pocketbase/daos"
|
||||||
|
"github.com/pocketbase/pocketbase/tests"
|
||||||
|
"htwkalender/model"
|
||||||
|
mockTime "htwkalender/service/functions/time"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const testDataDir = "./mockData"
|
||||||
|
|
||||||
|
func TestClearFeeds(t *testing.T) {
|
||||||
|
|
||||||
|
setupTestApp := func(t *testing.T) *daos.Dao {
|
||||||
|
testApp, err := tests.NewTestApp(testDataDir)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
dao := daos.New(testApp.Dao().DB())
|
||||||
|
return dao
|
||||||
|
}
|
||||||
|
|
||||||
|
type args struct {
|
||||||
|
db *daos.Dao
|
||||||
|
months int
|
||||||
|
mockClock mockTime.MockClock
|
||||||
|
}
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
args args
|
||||||
|
want int
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "TestClearFeeds",
|
||||||
|
args: args{
|
||||||
|
db: setupTestApp(t),
|
||||||
|
months: 6,
|
||||||
|
mockClock: mockTime.MockClock{
|
||||||
|
NowTime: time.Date(2023, 12, 1, 0, 0, 0, 0, time.UTC),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "TestClearAllFeeds",
|
||||||
|
args: args{
|
||||||
|
db: setupTestApp(t),
|
||||||
|
months: 1,
|
||||||
|
mockClock: mockTime.MockClock{
|
||||||
|
NowTime: time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "TestClearFeedsClearBeforeRetrievedTime",
|
||||||
|
args: args{
|
||||||
|
db: setupTestApp(t),
|
||||||
|
months: 1,
|
||||||
|
mockClock: mockTime.MockClock{
|
||||||
|
NowTime: time.Date(2010, 1, 1, 0, 0, 0, 0, time.UTC),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: 3,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range testCases {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
ClearFeeds(tt.args.db, tt.args.months, tt.args.mockClock)
|
||||||
|
// count all feeds in db
|
||||||
|
var feeds []*model.Feed
|
||||||
|
err := tt.args.db.DB().Select("id").From("feeds").All(&feeds)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if got := len(feeds); got != tt.want {
|
||||||
|
t.Errorf("ClearFeeds() = %v, want %v", got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
BIN
backend/service/feed/mockData/data.db
Normal file
BIN
backend/service/feed/mockData/data.db
Normal file
Binary file not shown.
BIN
backend/service/feed/mockData/logs.db
Normal file
BIN
backend/service/feed/mockData/logs.db
Normal file
Binary file not shown.
10
backend/service/functions/time/mockClock.go
Normal file
10
backend/service/functions/time/mockClock.go
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
package time
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
type MockClock struct {
|
||||||
|
NowTime time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m MockClock) Now() time.Time { return m.NowTime }
|
||||||
|
func (MockClock) After(d time.Duration) <-chan time.Time { return time.After(d) }
|
8
backend/service/functions/time/realClock.go
Normal file
8
backend/service/functions/time/realClock.go
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
package time
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
type RealClock struct{}
|
||||||
|
|
||||||
|
func (RealClock) Now() time.Time { return time.Now() }
|
||||||
|
func (RealClock) After(d time.Duration) <-chan time.Time { return time.After(d) }
|
8
backend/service/functions/time/time.go
Normal file
8
backend/service/functions/time/time.go
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
package time
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
type Clock interface {
|
||||||
|
Now() time.Time
|
||||||
|
After(d time.Duration) <-chan time.Time
|
||||||
|
}
|
@@ -1,13 +1,8 @@
|
|||||||
import { Module } from "../model/module";
|
import { Module } from "../model/module";
|
||||||
|
|
||||||
export async function fetchModule(module: Module): Promise<Module> {
|
export async function fetchModule(module: Module): Promise<Module> {
|
||||||
const request = new Request("/api/module", {
|
// request to the backend on /api/module with query parameters name as the module name
|
||||||
method: "POST",
|
const request = new Request("/api/module?uuid=" + module.uuid);
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
body: JSON.stringify(module),
|
|
||||||
});
|
|
||||||
|
|
||||||
return await fetch(request)
|
return await fetch(request)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
|
@@ -6,9 +6,13 @@ export async function getCalender(token: string): Promise<Module[]> {
|
|||||||
method: "GET",
|
method: "GET",
|
||||||
});
|
});
|
||||||
|
|
||||||
return await fetch(request)
|
return await fetch(request).then((response) => {
|
||||||
.then((response) => {
|
if (response.ok) {
|
||||||
return response.json();
|
return response
|
||||||
})
|
.json()
|
||||||
.then((calendarResponse: Calendar) => calendarResponse.modules);
|
.then((calendarResponse: Calendar) => calendarResponse.modules);
|
||||||
|
} else {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
@@ -1,13 +1,16 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { defineAsyncComponent, ref, Ref } from "vue";
|
import { defineAsyncComponent, ref, Ref } from "vue";
|
||||||
import { Module } from "../../model/module";
|
import { Module } from "../../model/module.ts";
|
||||||
import { fetchAllModules } from "../../api/fetchCourse";
|
import { fetchAllModules } from "../../api/fetchCourse.ts";
|
||||||
import moduleStore from "../../store/moduleStore";
|
import moduleStore from "../../store/moduleStore";
|
||||||
import { MultiSelectAllChangeEvent } from "primevue/multiselect";
|
import { MultiSelectAllChangeEvent } from "primevue/multiselect";
|
||||||
import router from "../../router";
|
import router from "../../router";
|
||||||
import { fetchModule } from "../../api/fetchModule";
|
import { fetchModule } from "../../api/fetchModule.ts";
|
||||||
import { useDialog } from "primevue/usedialog";
|
import { useDialog } from "primevue/usedialog";
|
||||||
|
import { useI18n } from "vue-i18n";
|
||||||
|
|
||||||
const dialog = useDialog();
|
const dialog = useDialog();
|
||||||
|
const { t } = useI18n({ useScope: "global" });
|
||||||
|
|
||||||
const fetchedModules = async () => {
|
const fetchedModules = async () => {
|
||||||
return await fetchAllModules();
|
return await fetchAllModules();
|
||||||
@@ -70,14 +73,29 @@ const onSelectAllChange = (event: MultiSelectAllChangeEvent) => {
|
|||||||
function selectChange() {
|
function selectChange() {
|
||||||
selectAll.value = selectedModules.value.length === modules.value.length;
|
selectAll.value = selectedModules.value.length === modules.value.length;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function itemsLabel(selectedModules: Module[]): string {
|
||||||
|
return (selectedModules ? selectedModules.length : 0) != 1
|
||||||
|
? t("additionalModules.modules")
|
||||||
|
: t("additionalModules.module");
|
||||||
|
}
|
||||||
|
|
||||||
|
function itemsLabelWithNumber(selectedModules: Module[]): string {
|
||||||
|
return (
|
||||||
|
selectedModules.length.toString() +
|
||||||
|
" " +
|
||||||
|
itemsLabel(selectedModules) +
|
||||||
|
" " +
|
||||||
|
t("additionalModules.dropDownFooterSelected")
|
||||||
|
);
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="flex flex-column">
|
<div class="flex flex-column">
|
||||||
<div class="flex align-items-center justify-content-center h-4rem m-2">
|
<div class="flex align-items-center justify-content-center h-4rem m-2">
|
||||||
<h3>
|
<h3>
|
||||||
Select additional Modules that are not listed in the regular semester
|
{{ $t("additionalModules.subTitle") }}
|
||||||
for your Course
|
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
<div class="card flex align-items-center justify-content-center m-2">
|
<div class="card flex align-items-center justify-content-center m-2">
|
||||||
@@ -90,8 +108,10 @@ function selectChange() {
|
|||||||
:virtual-scroller-options="{ itemSize: 70 }"
|
:virtual-scroller-options="{ itemSize: 70 }"
|
||||||
class="custom-multiselect"
|
class="custom-multiselect"
|
||||||
filter
|
filter
|
||||||
placeholder="Select additional modules"
|
:placeholder="$t('additionalModules.dropDown')"
|
||||||
:auto-filter-focus="true"
|
:auto-filter-focus="true"
|
||||||
|
:show-toggle-all="false"
|
||||||
|
:selected-items-label="itemsLabelWithNumber(selectedModules)"
|
||||||
@change="selectChange()"
|
@change="selectChange()"
|
||||||
@selectall-change="onSelectAllChange($event)"
|
@selectall-change="onSelectAllChange($event)"
|
||||||
>
|
>
|
||||||
@@ -104,6 +124,7 @@ function selectChange() {
|
|||||||
</div>
|
</div>
|
||||||
<div class="flex align-items-center justify-content-center ml-2">
|
<div class="flex align-items-center justify-content-center ml-2">
|
||||||
<Button
|
<Button
|
||||||
|
class="small-button"
|
||||||
icon="pi pi-info"
|
icon="pi pi-info"
|
||||||
severity="secondary"
|
severity="secondary"
|
||||||
rounded
|
rounded
|
||||||
@@ -127,7 +148,9 @@ function selectChange() {
|
|||||||
</MultiSelect>
|
</MultiSelect>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex align-items-center justify-content-center h-4rem m-2">
|
<div class="flex align-items-center justify-content-center h-4rem m-2">
|
||||||
<Button @click="nextStep()">Next Step</Button>
|
<Button @click="nextStep()">{{
|
||||||
|
$t("additionalModules.nextStep")
|
||||||
|
}}</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -140,4 +163,10 @@ function selectChange() {
|
|||||||
:deep(.custom-multiselect li) {
|
:deep(.custom-multiselect li) {
|
||||||
height: unset;
|
height: unset;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.small-button.p-button {
|
||||||
|
width: 2rem;
|
||||||
|
height: 2rem;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@@ -1,13 +1,15 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed, Ref, ref } from "vue";
|
import { computed, Ref, ref } from "vue";
|
||||||
import { Module } from "../../model/module";
|
import { Module } from "../../model/module.ts";
|
||||||
import moduleStore from "../../store/moduleStore";
|
import moduleStore from "../../store/moduleStore";
|
||||||
import { fetchAllModules } from "../../api/fetchCourse";
|
import { fetchAllModules } from "../../api/fetchCourse.ts";
|
||||||
import { saveIndividualFeed } from "../../api/createFeed";
|
import { saveIndividualFeed } from "../../api/createFeed.ts";
|
||||||
import tokenStore from "../../store/tokenStore";
|
import tokenStore from "../../store/tokenStore";
|
||||||
import router from "../../router";
|
import router from "../../router";
|
||||||
import ModuleTemplateDialog from "../ModuleTemplateDialog.vue";
|
import ModuleTemplateDialog from "../ModuleTemplateDialog.vue";
|
||||||
import { onlyWhitespace } from "../../helpers/strings.ts";
|
import { onlyWhitespace } from "../../helpers/strings.ts";
|
||||||
|
import { useI18n } from "vue-i18n";
|
||||||
|
const { t } = useI18n({ useScope: "global" });
|
||||||
|
|
||||||
const store = moduleStore();
|
const store = moduleStore();
|
||||||
const tableData = computed(() =>
|
const tableData = computed(() =>
|
||||||
@@ -20,9 +22,9 @@ const tableData = computed(() =>
|
|||||||
);
|
);
|
||||||
|
|
||||||
const columns = ref([
|
const columns = ref([
|
||||||
{ field: "Course", header: "Course" },
|
{ field: "Course", header: t("moduleInformation.course") },
|
||||||
{ field: "Module", header: "Module" },
|
{ field: "Module", header: t("moduleInformation.module") },
|
||||||
{ field: "Reminder", header: "Reminder" },
|
{ field: "Reminder", header: t("renameModules.reminder") },
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const fetchedModules = async () => {
|
const fetchedModules = async () => {
|
||||||
@@ -52,7 +54,7 @@ async function finalStep() {
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex flex-column card-container mx-8 mt-2">
|
<div class="flex flex-column card-container mx-8 mt-2">
|
||||||
<div class="flex align-items-center justify-content-center h-4rem m-2">
|
<div class="flex align-items-center justify-content-center h-4rem m-2">
|
||||||
<h3>Rename your selected Modules to your liking.</h3>
|
<h3>{{ $t("renameModules.subTitle") }}</h3>
|
||||||
<ModuleTemplateDialog />
|
<ModuleTemplateDialog />
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
@@ -66,7 +68,7 @@ async function finalStep() {
|
|||||||
>
|
>
|
||||||
<template #header>
|
<template #header>
|
||||||
<div class="flex align-items-center justify-content-end">
|
<div class="flex align-items-center justify-content-end">
|
||||||
Enable all notifications:
|
{{ $t("renameModules.enableAllNotifications") }}
|
||||||
<InputSwitch
|
<InputSwitch
|
||||||
class="mx-4"
|
class="mx-4"
|
||||||
:model-value="
|
:model-value="
|
||||||
@@ -153,7 +155,7 @@ async function finalStep() {
|
|||||||
<div
|
<div
|
||||||
class="flex align-items-center justify-content-center border-round m-2"
|
class="flex align-items-center justify-content-center border-round m-2"
|
||||||
>
|
>
|
||||||
<Button label="Save Calendar" @click="finalStep()" />
|
<Button @click="finalStep()">{{ $t("renameModules.nextStep") }}</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
@@ -49,7 +49,8 @@
|
|||||||
"invalidToken": "Ungültiger Token",
|
"invalidToken": "Ungültiger Token",
|
||||||
"headline": "Bearbeite deinen HTWKalender",
|
"headline": "Bearbeite deinen HTWKalender",
|
||||||
"subTitle": "Füge deinen Link oder Token ein um den Kalender zu bearbeiten",
|
"subTitle": "Füge deinen Link oder Token ein um den Kalender zu bearbeiten",
|
||||||
"loadCalendar": "Kalender laden"
|
"loadCalendar": "Kalender laden",
|
||||||
|
"noCalendarFound": "Keinen Kalender gefunden"
|
||||||
},
|
},
|
||||||
"additionalModules": {
|
"additionalModules": {
|
||||||
"subTitle": "Füge weitere Module hinzu die nicht in deinem Studiengang enthalten sind.",
|
"subTitle": "Füge weitere Module hinzu die nicht in deinem Studiengang enthalten sind.",
|
||||||
@@ -63,7 +64,7 @@
|
|||||||
"reminder": "Erinnerung",
|
"reminder": "Erinnerung",
|
||||||
"enableAllNotifications": "Alle Benachrichtigungen aktivieren",
|
"enableAllNotifications": "Alle Benachrichtigungen aktivieren",
|
||||||
"subTitle": "Konfigurieren Sie die ausgewählten Module nach Ihren Wünschen.",
|
"subTitle": "Konfigurieren Sie die ausgewählten Module nach Ihren Wünschen.",
|
||||||
"nextStep": "Weiter"
|
"nextStep": "Speichern"
|
||||||
},
|
},
|
||||||
"moduleTemplateDialog": {
|
"moduleTemplateDialog": {
|
||||||
"explanationOne": "Hier können Module nach Wunsch umbenannt werden, welche dann als Anzeigename im Kalender dargestellt werden.",
|
"explanationOne": "Hier können Module nach Wunsch umbenannt werden, welche dann als Anzeigename im Kalender dargestellt werden.",
|
||||||
|
@@ -49,7 +49,8 @@
|
|||||||
"invalidToken": "invalid token",
|
"invalidToken": "invalid token",
|
||||||
"headline": "edit your HTWKalender",
|
"headline": "edit your HTWKalender",
|
||||||
"subTitle": "please enter your link or calendar token",
|
"subTitle": "please enter your link or calendar token",
|
||||||
"loadCalendar": "load calendar"
|
"loadCalendar": "load calendar",
|
||||||
|
"noCalendarFound": "no calendar found"
|
||||||
},
|
},
|
||||||
"additionalModules": {
|
"additionalModules": {
|
||||||
"subTitle": "Select additional Modules that are not listed in the regular semester for your Course",
|
"subTitle": "Select additional Modules that are not listed in the regular semester for your Course",
|
||||||
@@ -63,7 +64,7 @@
|
|||||||
"reminder": "reminder",
|
"reminder": "reminder",
|
||||||
"enableAllNotifications": "enable all notifications",
|
"enableAllNotifications": "enable all notifications",
|
||||||
"subTitle": "Configure your selected Modules to your liking.",
|
"subTitle": "Configure your selected Modules to your liking.",
|
||||||
"nextStep": "next step"
|
"nextStep": "Save"
|
||||||
},
|
},
|
||||||
"moduleTemplateDialog": {
|
"moduleTemplateDialog": {
|
||||||
"explanationOne": "Here you can rename your modules to your liking. This will be the name of the event in your calendar.",
|
"explanationOne": "Here you can rename your modules to your liking. This will be the name of the event in your calendar.",
|
||||||
|
@@ -7,8 +7,10 @@ import { FilterMatchMode } from "primevue/api";
|
|||||||
import { useDialog } from "primevue/usedialog";
|
import { useDialog } from "primevue/usedialog";
|
||||||
import router from "../router";
|
import router from "../router";
|
||||||
import { fetchModule } from "../api/fetchModule.ts";
|
import { fetchModule } from "../api/fetchModule.ts";
|
||||||
|
import { useI18n } from "vue-i18n";
|
||||||
|
|
||||||
const dialog = useDialog();
|
const dialog = useDialog();
|
||||||
|
const { t } = useI18n({ useScope: "global" });
|
||||||
|
|
||||||
const fetchedModules = async () => {
|
const fetchedModules = async () => {
|
||||||
return await fetchAllModules();
|
return await fetchAllModules();
|
||||||
@@ -111,11 +113,19 @@ function selectChange(event : MultiSelectChangeEvent) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function itemsLabel(selectedModules: Module[]): string {
|
function itemsLabel(selectedModules: Module[]): string {
|
||||||
return (selectedModules ? selectedModules.length : 0) != 1 ? t("additionalModules.modules") : t("additionalModules.module");
|
return (selectedModules ? selectedModules.length : 0) != 1
|
||||||
|
? t("additionalModules.modules")
|
||||||
|
: t("additionalModules.module");
|
||||||
}
|
}
|
||||||
|
|
||||||
function itemsLabelWithNumber(selectedModules: Module[]): string {
|
function itemsLabelWithNumber(selectedModules: Module[]): string {
|
||||||
return selectedModules.length.toString() + " " + itemsLabel(selectedModules) + " " + t("additionalModules.dropDownFooterSelected");
|
return (
|
||||||
|
selectedModules.length.toString() +
|
||||||
|
" " +
|
||||||
|
itemsLabel(selectedModules) +
|
||||||
|
" " +
|
||||||
|
t("additionalModules.dropDownFooterSelected")
|
||||||
|
);
|
||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
</script>
|
</script>
|
||||||
@@ -228,11 +238,11 @@ function itemsLabelWithNumber(selectedModules: Module[]): string {
|
|||||||
:placeholder="$t('additionalModules.dropDown')"
|
:placeholder="$t('additionalModules.dropDown')"
|
||||||
:auto-filter-focus="true"
|
:auto-filter-focus="true"
|
||||||
:show-toggle-all="false"
|
:show-toggle-all="false"
|
||||||
|
:selected-items-label="itemsLabelWithNumber(selectedModules)"
|
||||||
@change="selectChange()"
|
@change="selectChange()"
|
||||||
placeholder="Select additional modules"
|
placeholder="Select additional modules"
|
||||||
@change="selectChange($event)"
|
@change="selectChange($event)"
|
||||||
@selectall-change="onSelectAllChange($event)"
|
@selectall-change="onSelectAllChange($event)"
|
||||||
:selectedItemsLabel="itemsLabelWithNumber(selectedModules)"
|
|
||||||
>
|
>
|
||||||
<template #option="slotProps">
|
<template #option="slotProps">
|
||||||
<div class="flex justify-content-between w-full">
|
<div class="flex justify-content-between w-full">
|
||||||
@@ -243,6 +253,7 @@ function itemsLabelWithNumber(selectedModules: Module[]): string {
|
|||||||
</div>
|
</div>
|
||||||
<div class="flex align-items-center justify-content-center ml-2">
|
<div class="flex align-items-center justify-content-center ml-2">
|
||||||
<Button
|
<Button
|
||||||
|
class="small-button"
|
||||||
icon="pi pi-info"
|
icon="pi pi-info"
|
||||||
severity="secondary"
|
severity="secondary"
|
||||||
rounded
|
rounded
|
||||||
@@ -258,8 +269,10 @@ function itemsLabelWithNumber(selectedModules: Module[]): string {
|
|||||||
<template #footer>
|
<template #footer>
|
||||||
<div class="py-2 px-3">
|
<div class="py-2 px-3">
|
||||||
<b>{{ selectedModules ? selectedModules.length : 0 }}</b>
|
<b>{{ selectedModules ? selectedModules.length : 0 }}</b>
|
||||||
{{ itemsLabel(selectedModules) }}
|
item{{
|
||||||
{{ $t("additionalModules.dropDownFooterSelected") }}
|
(selectedModules ? selectedModules.length : 0) > 1 ? "s" : ""
|
||||||
|
}}
|
||||||
|
selected.
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</MultiSelect>
|
</MultiSelect>
|
||||||
@@ -280,4 +293,10 @@ function itemsLabelWithNumber(selectedModules: Module[]): string {
|
|||||||
:deep(.custom-multiselect li) {
|
:deep(.custom-multiselect li) {
|
||||||
height: unset;
|
height: unset;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.small-button.p-button {
|
||||||
|
width: 2rem;
|
||||||
|
height: 2rem;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@@ -46,14 +46,22 @@ function loadCalendar(): void {
|
|||||||
moduleStore().removeAllModules();
|
moduleStore().removeAllModules();
|
||||||
tokenStore().setToken(token.value);
|
tokenStore().setToken(token.value);
|
||||||
|
|
||||||
getCalender(token.value).then((data) => {
|
getCalender(token.value).then((data: Module[]) => {
|
||||||
|
if (data.length > 0) {
|
||||||
data.forEach((module) => {
|
data.forEach((module) => {
|
||||||
moduleStore().addModule(module);
|
moduleStore().addModule(module);
|
||||||
});
|
});
|
||||||
modules.value = moduleStore().modules;
|
modules.value = moduleStore().modules;
|
||||||
});
|
|
||||||
|
|
||||||
router.push("/edit-additional-modules");
|
router.push("/edit-additional-modules");
|
||||||
|
} else {
|
||||||
|
toast.add({
|
||||||
|
severity: "error",
|
||||||
|
summary: t("editCalendarView.error"),
|
||||||
|
detail: t("editCalendarView.noCalendarFound"),
|
||||||
|
life: 3000,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user