Merge branch 'main' into 15-calendar-preview

# Conflicts:
#	frontend/src/view/AdditionalModules.vue
#	frontend/src/view/EditCalendarView.vue
This commit is contained in:
masterElmar
2023-11-28 19:16:10 +01:00
27 changed files with 1006 additions and 89 deletions

View File

@@ -35,6 +35,14 @@ htwkalender-demo
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.
This should be done quick in a few seconds (0-5s).
When you execute the command again, it will update the groups in the

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

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

View File

@@ -1,9 +1,13 @@
package model
import "github.com/pocketbase/pocketbase/models"
import (
"github.com/pocketbase/pocketbase/models"
"github.com/pocketbase/pocketbase/tools/types"
)
type Feed struct {
Modules string `db:"modules" json:"modules"`
Modules string `db:"modules" json:"modules"`
Retrieved types.DateTime `db:"retrieved" json:"retrieved"`
models.BaseModel
}

View File

@@ -1,10 +1,10 @@
package model
type Module struct {
UUID string `json:"uuid"`
Name string `json:"name"`
Prof string `json:"prof"`
Course string `json:"course"`
Semester string `json:"semester"`
UUID string `json:"uuid" db:"uuid"`
Name string `json:"name" db:"Name"`
Prof string `json:"prof" db:"Prof"`
Course string `json:"course" db:"course"`
Semester string `json:"semester" db:"semester"`
Events Events `json:"events"`
}

View File

@@ -11,30 +11,150 @@ paths:
/api/fetchPlans:
get:
summary: Fetch Seminar Plans
security:
- ApiKeyAuth: []
responses:
'200':
description: Successful response
/api/fetchGroups:
get:
summary: Fetch Seminar Groups
security:
- ApiKeyAuth: []
responses:
'200':
description: Successful response
/api/modules:
delete:
summary: Delete Module
security:
- ApiKeyAuth: []
responses:
'200':
description: Successful response
/api/rooms:
get:
summary: Get Rooms
responses:
'200':
description: Successful response
/api/feedURL:
/api/schedule/day:
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:
'200':
description: Successful response
/api/feed:
get:
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:
'200':
description: Successful response
@@ -51,3 +171,52 @@ paths:
responses:
'200':
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

View File

@@ -1,7 +1,6 @@
package service
import (
"htwkalender/model"
"htwkalender/service/events"
"htwkalender/service/fetch"
"htwkalender/service/ical"
@@ -26,6 +25,7 @@ func AddRoutes(app *pocketbase.PocketBase) {
},
Middlewares: []echo.MiddlewareFunc{
apis.ActivityLogger(app),
apis.RequireAdminAuth(),
},
})
if err != nil {
@@ -61,6 +61,7 @@ func AddRoutes(app *pocketbase.PocketBase) {
},
Middlewares: []echo.MiddlewareFunc{
apis.ActivityLogger(app),
apis.RequireAdminAuth(),
},
})
if err != nil {
@@ -212,18 +213,11 @@ func AddRoutes(app *pocketbase.PocketBase) {
app.OnBeforeServe().Add(func(e *core.ServeEvent) error {
_, err := e.Router.AddRoute(echo.Route{
Method: http.MethodPost,
Method: http.MethodGet,
Path: "/api/module",
Handler: func(c echo.Context) error {
var requestModule model.Module
if err := c.Bind(&requestModule); err != nil {
return apis.NewBadRequestError("Failed to read request body", err)
}
module, err := events.GetModuleByName(app, requestModule)
requestModule := c.QueryParam("uuid")
module, err := events.GetModuleByUUID(app, requestModule)
if err != nil {
return c.JSON(400, err)
} else {
@@ -286,7 +280,7 @@ func AddRoutes(app *pocketbase.PocketBase) {
app.OnBeforeServe().Add(func(e *core.ServeEvent) error {
_, err := e.Router.AddRoute(echo.Route{
Method: http.MethodGet,
Path: "/api/feed/migrate",
Path: "/api/feeds/migrate",
Handler: func(c echo.Context) error {
err := ical.MigrateFeedJson(app)

View File

@@ -4,8 +4,9 @@ import (
"github.com/pocketbase/pocketbase"
"github.com/pocketbase/pocketbase/core"
"github.com/pocketbase/pocketbase/tools/cron"
"htwkalender/service/events"
"log"
"htwkalender/service/course"
"htwkalender/service/feed"
"htwkalender/service/functions/time"
)
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 10 minutes update all courses (5 segments - minute, hour, day, month, weekday) "*/10 * * * *"
scheduler.MustAdd("updateCourse", "0 */3 * * *", func() {
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")
}
}
course.UpdateCourse(app)
})
// Every sunday at 3am clean all courses (5 segments - minute, hour, day, month, weekday) "0 3 * * 0"
scheduler.MustAdd("cleanFeeds", "0 3 * * 0", func() {
// clean feeds older than 6 months
feed.ClearFeeds(app.Dao(), 6, time.RealClock{})
})
scheduler.Start()
return nil
})

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

View File

@@ -196,6 +196,18 @@ func DeleteAllEvents(app *pocketbase.PocketBase) error {
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) {
var events model.Events

View File

@@ -1,10 +1,11 @@
package db
import (
"github.com/pocketbase/dbx"
"github.com/pocketbase/pocketbase"
"github.com/pocketbase/pocketbase/daos"
"github.com/pocketbase/pocketbase/models"
"htwkalender/model"
"time"
)
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) {
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 {
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
}
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
}

View File

@@ -42,11 +42,8 @@ func GetAllModulesDistinct(app *pocketbase.PocketBase, c echo.Context) error {
}
}
// GetModuleByName returns a module by its name
// If the module does not exist, an error is returned
// 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) {
func GetModuleByUUID(app *pocketbase.PocketBase, uuid string) (model.Module, error) {
module, err := db.FindModuleByUUID(app, uuid)
events, err := db.FindAllEventsByModule(app, module)
if err != nil || len(events) == 0 {

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

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

Binary file not shown.

Binary file not shown.

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

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

View File

@@ -0,0 +1,8 @@
package time
import "time"
type Clock interface {
Now() time.Time
After(d time.Duration) <-chan time.Time
}

View File

@@ -1,13 +1,8 @@
import { Module } from "../model/module";
export async function fetchModule(module: Module): Promise<Module> {
const request = new Request("/api/module", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(module),
});
// request to the backend on /api/module with query parameters name as the module name
const request = new Request("/api/module?uuid=" + module.uuid);
return await fetch(request)
.then((response) => {

View File

@@ -6,9 +6,13 @@ export async function getCalender(token: string): Promise<Module[]> {
method: "GET",
});
return await fetch(request)
.then((response) => {
return response.json();
})
.then((calendarResponse: Calendar) => calendarResponse.modules);
return await fetch(request).then((response) => {
if (response.ok) {
return response
.json()
.then((calendarResponse: Calendar) => calendarResponse.modules);
} else {
return [];
}
});
}

View File

@@ -1,13 +1,16 @@
<script lang="ts" setup>
import { defineAsyncComponent, ref, Ref } from "vue";
import { Module } from "../../model/module";
import { fetchAllModules } from "../../api/fetchCourse";
import { Module } from "../../model/module.ts";
import { fetchAllModules } from "../../api/fetchCourse.ts";
import moduleStore from "../../store/moduleStore";
import { MultiSelectAllChangeEvent } from "primevue/multiselect";
import router from "../../router";
import { fetchModule } from "../../api/fetchModule";
import { fetchModule } from "../../api/fetchModule.ts";
import { useDialog } from "primevue/usedialog";
import { useI18n } from "vue-i18n";
const dialog = useDialog();
const { t } = useI18n({ useScope: "global" });
const fetchedModules = async () => {
return await fetchAllModules();
@@ -70,14 +73,29 @@ const onSelectAllChange = (event: MultiSelectAllChangeEvent) => {
function selectChange() {
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>
<template>
<div class="flex flex-column">
<div class="flex align-items-center justify-content-center h-4rem m-2">
<h3>
Select additional Modules that are not listed in the regular semester
for your Course
{{ $t("additionalModules.subTitle") }}
</h3>
</div>
<div class="card flex align-items-center justify-content-center m-2">
@@ -90,8 +108,10 @@ function selectChange() {
:virtual-scroller-options="{ itemSize: 70 }"
class="custom-multiselect"
filter
placeholder="Select additional modules"
:placeholder="$t('additionalModules.dropDown')"
:auto-filter-focus="true"
:show-toggle-all="false"
:selected-items-label="itemsLabelWithNumber(selectedModules)"
@change="selectChange()"
@selectall-change="onSelectAllChange($event)"
>
@@ -104,6 +124,7 @@ function selectChange() {
</div>
<div class="flex align-items-center justify-content-center ml-2">
<Button
class="small-button"
icon="pi pi-info"
severity="secondary"
rounded
@@ -127,7 +148,9 @@ function selectChange() {
</MultiSelect>
</div>
<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>
</template>
@@ -140,4 +163,10 @@ function selectChange() {
:deep(.custom-multiselect li) {
height: unset;
}
.small-button.p-button {
width: 2rem;
height: 2rem;
padding: 0;
}
</style>

View File

@@ -1,13 +1,15 @@
<script lang="ts" setup>
import { computed, Ref, ref } from "vue";
import { Module } from "../../model/module";
import { Module } from "../../model/module.ts";
import moduleStore from "../../store/moduleStore";
import { fetchAllModules } from "../../api/fetchCourse";
import { saveIndividualFeed } from "../../api/createFeed";
import { fetchAllModules } from "../../api/fetchCourse.ts";
import { saveIndividualFeed } from "../../api/createFeed.ts";
import tokenStore from "../../store/tokenStore";
import router from "../../router";
import ModuleTemplateDialog from "../ModuleTemplateDialog.vue";
import { onlyWhitespace } from "../../helpers/strings.ts";
import { useI18n } from "vue-i18n";
const { t } = useI18n({ useScope: "global" });
const store = moduleStore();
const tableData = computed(() =>
@@ -20,9 +22,9 @@ const tableData = computed(() =>
);
const columns = ref([
{ field: "Course", header: "Course" },
{ field: "Module", header: "Module" },
{ field: "Reminder", header: "Reminder" },
{ field: "Course", header: t("moduleInformation.course") },
{ field: "Module", header: t("moduleInformation.module") },
{ field: "Reminder", header: t("renameModules.reminder") },
]);
const fetchedModules = async () => {
@@ -52,7 +54,7 @@ async function finalStep() {
<template>
<div class="flex flex-column card-container mx-8 mt-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 />
</div>
<div
@@ -66,7 +68,7 @@ async function finalStep() {
>
<template #header>
<div class="flex align-items-center justify-content-end">
Enable all notifications:
{{ $t("renameModules.enableAllNotifications") }}
<InputSwitch
class="mx-4"
:model-value="
@@ -153,7 +155,7 @@ async function finalStep() {
<div
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>
</template>

View File

@@ -49,7 +49,8 @@
"invalidToken": "Ungültiger Token",
"headline": "Bearbeite deinen HTWKalender",
"subTitle": "Füge deinen Link oder Token ein um den Kalender zu bearbeiten",
"loadCalendar": "Kalender laden"
"loadCalendar": "Kalender laden",
"noCalendarFound": "Keinen Kalender gefunden"
},
"additionalModules": {
"subTitle": "Füge weitere Module hinzu die nicht in deinem Studiengang enthalten sind.",
@@ -63,7 +64,7 @@
"reminder": "Erinnerung",
"enableAllNotifications": "Alle Benachrichtigungen aktivieren",
"subTitle": "Konfigurieren Sie die ausgewählten Module nach Ihren Wünschen.",
"nextStep": "Weiter"
"nextStep": "Speichern"
},
"moduleTemplateDialog": {
"explanationOne": "Hier können Module nach Wunsch umbenannt werden, welche dann als Anzeigename im Kalender dargestellt werden.",

View File

@@ -49,7 +49,8 @@
"invalidToken": "invalid token",
"headline": "edit your HTWKalender",
"subTitle": "please enter your link or calendar token",
"loadCalendar": "load calendar"
"loadCalendar": "load calendar",
"noCalendarFound": "no calendar found"
},
"additionalModules": {
"subTitle": "Select additional Modules that are not listed in the regular semester for your Course",
@@ -63,7 +64,7 @@
"reminder": "reminder",
"enableAllNotifications": "enable all notifications",
"subTitle": "Configure your selected Modules to your liking.",
"nextStep": "next step"
"nextStep": "Save"
},
"moduleTemplateDialog": {
"explanationOne": "Here you can rename your modules to your liking. This will be the name of the event in your calendar.",

View File

@@ -7,8 +7,10 @@ import { FilterMatchMode } from "primevue/api";
import { useDialog } from "primevue/usedialog";
import router from "../router";
import { fetchModule } from "../api/fetchModule.ts";
import { useI18n } from "vue-i18n";
const dialog = useDialog();
const { t } = useI18n({ useScope: "global" });
const fetchedModules = async () => {
return await fetchAllModules();
@@ -111,11 +113,19 @@ function selectChange(event : MultiSelectChangeEvent) {
}
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 {
return selectedModules.length.toString() + " " + itemsLabel(selectedModules) + " " + t("additionalModules.dropDownFooterSelected");
return (
selectedModules.length.toString() +
" " +
itemsLabel(selectedModules) +
" " +
t("additionalModules.dropDownFooterSelected")
);
}
*/
</script>
@@ -228,11 +238,11 @@ function itemsLabelWithNumber(selectedModules: Module[]): string {
:placeholder="$t('additionalModules.dropDown')"
:auto-filter-focus="true"
:show-toggle-all="false"
:selected-items-label="itemsLabelWithNumber(selectedModules)"
@change="selectChange()"
placeholder="Select additional modules"
@change="selectChange($event)"
@selectall-change="onSelectAllChange($event)"
:selectedItemsLabel="itemsLabelWithNumber(selectedModules)"
>
<template #option="slotProps">
<div class="flex justify-content-between w-full">
@@ -243,6 +253,7 @@ function itemsLabelWithNumber(selectedModules: Module[]): string {
</div>
<div class="flex align-items-center justify-content-center ml-2">
<Button
class="small-button"
icon="pi pi-info"
severity="secondary"
rounded
@@ -258,8 +269,10 @@ function itemsLabelWithNumber(selectedModules: Module[]): string {
<template #footer>
<div class="py-2 px-3">
<b>{{ selectedModules ? selectedModules.length : 0 }}</b>
{{ itemsLabel(selectedModules) }}
{{ $t("additionalModules.dropDownFooterSelected") }}
item{{
(selectedModules ? selectedModules.length : 0) > 1 ? "s" : ""
}}
selected.
</div>
</template>
</MultiSelect>
@@ -280,4 +293,10 @@ function itemsLabelWithNumber(selectedModules: Module[]): string {
:deep(.custom-multiselect li) {
height: unset;
}
.small-button.p-button {
width: 2rem;
height: 2rem;
padding: 0;
}
</style>

View File

@@ -46,14 +46,22 @@ function loadCalendar(): void {
moduleStore().removeAllModules();
tokenStore().setToken(token.value);
getCalender(token.value).then((data) => {
data.forEach((module) => {
moduleStore().addModule(module);
});
modules.value = moduleStore().modules;
getCalender(token.value).then((data: Module[]) => {
if (data.length > 0) {
data.forEach((module) => {
moduleStore().addModule(module);
});
modules.value = moduleStore().modules;
router.push("/edit-additional-modules");
} else {
toast.add({
severity: "error",
summary: t("editCalendarView.error"),
detail: t("editCalendarView.noCalendarFound"),
life: 3000,
});
}
});
router.push("/edit-additional-modules");
}
</script>