diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml
new file mode 100644
index 0000000..f986f2f
--- /dev/null
+++ b/.idea/codeStyles/Project.xml
@@ -0,0 +1,58 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml
new file mode 100644
index 0000000..79ee123
--- /dev/null
+++ b/.idea/codeStyles/codeStyleConfig.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/prettier.xml b/.idea/prettier.xml
new file mode 100644
index 0000000..b0c1c68
--- /dev/null
+++ b/.idea/prettier.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/README.md b/README.md
index 56444a6..ba7f48d 100644
--- a/README.md
+++ b/README.md
@@ -1,21 +1,56 @@
-# HTWK PLANNER
+# HTWKalender
Scrape information about Seminar Groups and dates.
### Run with
+
```bash
-go run . serve
+docker compose up --build
+```
+
+### Go to Admin UI
+
+➜ Webinterface: http://127.0.0.1/
+
+➜ Admin UI: http://127.0.0.1/_/
+
+➜ API: http://127.0.0.1/_/
+
+For first login use the following credentials:
+
+Email:
+```
+demo@htwkalender.de
+```
+
+Password:
+```
+htwkalender-demo
```
-### Go to Admin UI
-` ➜ Admin UI: http://127.0.0.1:8090/_/`
-- create account
-- import pb_schema.json
### Fetch Data from HTWK
-`http://127.0.0.1:8090/api/fetchPlans`
+Execute the following api calls to fetch data manually from HTWK and store it 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).
+When you execute the command again, it will update the groups in the
+database and only return new added groups.
+
+http://127.0.0.1/api/fetchGroups
+
+For fetching the plans, you can use the following command.
+This will fetch all plans for all groups and store the events in the database.
+It's done for all current existing events (ws/ss).
+The whole process takes a while (1-5min), depending on the amount of groups and events.
+Stay for this time on the page and wait for the response.
+
+http://127.0.0.1/api/fetchPlans
+
### View/Filter/Search in Admin UI
+
+If you want some easy first api endpoints and data views, you can use the Admin UI.
diff --git a/.gitignore b/backend/.gitignore
similarity index 100%
rename from .gitignore
rename to backend/.gitignore
diff --git a/backend/Dockerfile b/backend/Dockerfile
new file mode 100644
index 0000000..77f2aca
--- /dev/null
+++ b/backend/Dockerfile
@@ -0,0 +1,18 @@
+FROM golang:1.20.5-alpine
+
+# Set the Current Working Directory inside the container
+WORKDIR /app
+
+# Copy go mod and sum files
+COPY go.mod go.sum ./
+RUN go mod download
+
+# Copy the source from the current directory to the Working Directory inside the container
+COPY *.go ./
+COPY .. .
+
+# Build the Go app
+RUN CGO_ENABLED=0 GOOS=linux go build -o /htwkalender
+
+# Expose port 8080 to the outside world
+EXPOSE 8080
diff --git a/LICENSE b/backend/LICENSE
similarity index 100%
rename from LICENSE
rename to backend/LICENSE
diff --git a/backend/README.md b/backend/README.md
new file mode 100644
index 0000000..56444a6
--- /dev/null
+++ b/backend/README.md
@@ -0,0 +1,21 @@
+# HTWK PLANNER
+
+Scrape information about Seminar Groups and dates.
+
+### Run with
+```bash
+go run . serve
+```
+
+### Go to Admin UI
+
+` ➜ Admin UI: http://127.0.0.1:8090/_/`
+
+- create account
+- import pb_schema.json
+
+### Fetch Data from HTWK
+
+`http://127.0.0.1:8090/api/fetchPlans`
+
+### View/Filter/Search in Admin UI
diff --git a/go.mod b/backend/go.mod
similarity index 97%
rename from go.mod
rename to backend/go.mod
index ce19c60..9c711da 100644
--- a/go.mod
+++ b/backend/go.mod
@@ -4,7 +4,7 @@ go 1.20
require (
github.com/jordic/goics v0.0.0-20210404174824-5a0337b716a0
- github.com/labstack/echo/v5 v5.0.0-20230722203903-ec5b858dab61
+ github.com/pocketbase/dbx v1.10.0
github.com/pocketbase/pocketbase v0.17.5
golang.org/x/net v0.14.0
)
@@ -48,11 +48,11 @@ require (
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/jmespath/go-jmespath v0.4.0 // indirect
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
+ github.com/labstack/echo/v5 v5.0.0-20230722203903-ec5b858dab61 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.19 // indirect
github.com/mattn/go-sqlite3 v1.14.17 // indirect
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
- github.com/pocketbase/dbx v1.10.0 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/spf13/cast v1.5.1 // indirect
github.com/spf13/cobra v1.7.0 // indirect
diff --git a/go.sum b/backend/go.sum
similarity index 100%
rename from go.sum
rename to backend/go.sum
diff --git a/backend/main.go b/backend/main.go
new file mode 100644
index 0000000..fec7f0d
--- /dev/null
+++ b/backend/main.go
@@ -0,0 +1,30 @@
+package main
+
+import (
+ "github.com/pocketbase/pocketbase"
+ "github.com/pocketbase/pocketbase/plugins/migratecmd"
+ _ "htwk-planner/migrations"
+ "htwk-planner/service"
+ "log"
+ "os"
+ "strings"
+)
+
+func main() {
+ app := pocketbase.New()
+
+ // loosely check if it was executed using "go run"
+ isGoRun := strings.HasPrefix(os.Args[0], os.TempDir())
+
+ migratecmd.MustRegister(app, app.RootCmd, migratecmd.Config{
+ // enable auto creation of migration files when making collection changes in the Admin UI
+ // (the isGoRun check is to enable it only during development)
+ Automigrate: isGoRun,
+ })
+
+ service.AddRoutes(app)
+
+ if err := app.Start(); err != nil {
+ log.Fatal(err)
+ }
+}
diff --git a/backend/migrations/1695150679_collections_snapshot.go b/backend/migrations/1695150679_collections_snapshot.go
new file mode 100644
index 0000000..b9b701a
--- /dev/null
+++ b/backend/migrations/1695150679_collections_snapshot.go
@@ -0,0 +1,383 @@
+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": "_pb_users_auth_",
+ "created": "2023-09-19 17:30:50.598Z",
+ "updated": "2023-09-19 17:31:15.957Z",
+ "name": "users",
+ "type": "auth",
+ "system": false,
+ "schema": [
+ {
+ "system": false,
+ "id": "users_name",
+ "name": "name",
+ "type": "text",
+ "required": false,
+ "unique": false,
+ "options": {
+ "min": null,
+ "max": null,
+ "pattern": ""
+ }
+ },
+ {
+ "system": false,
+ "id": "users_avatar",
+ "name": "avatar",
+ "type": "file",
+ "required": 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
+ }
+ },
+ {
+ "id": "cfq9mqlmd97v8z5",
+ "created": "2023-09-19 17:31:15.957Z",
+ "updated": "2023-09-19 17:31:15.957Z",
+ "name": "groups",
+ "type": "base",
+ "system": false,
+ "schema": [
+ {
+ "system": false,
+ "id": "85msl21p",
+ "name": "university",
+ "type": "text",
+ "required": false,
+ "unique": false,
+ "options": {
+ "min": null,
+ "max": null,
+ "pattern": ""
+ }
+ },
+ {
+ "system": false,
+ "id": "2sii4dtp",
+ "name": "shortcut",
+ "type": "text",
+ "required": false,
+ "unique": false,
+ "options": {
+ "min": null,
+ "max": null,
+ "pattern": ""
+ }
+ },
+ {
+ "system": false,
+ "id": "uiwgo28f",
+ "name": "groupId",
+ "type": "text",
+ "required": false,
+ "unique": false,
+ "options": {
+ "min": null,
+ "max": null,
+ "pattern": ""
+ }
+ },
+ {
+ "system": false,
+ "id": "y0l1lrzs",
+ "name": "course",
+ "type": "text",
+ "required": false,
+ "unique": false,
+ "options": {
+ "min": null,
+ "max": null,
+ "pattern": ""
+ }
+ },
+ {
+ "system": false,
+ "id": "kr62mhbz",
+ "name": "faculty",
+ "type": "text",
+ "required": false,
+ "unique": false,
+ "options": {
+ "min": null,
+ "max": null,
+ "pattern": ""
+ }
+ },
+ {
+ "system": false,
+ "id": "ya6znpez",
+ "name": "facultyId",
+ "type": "text",
+ "required": 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-09-19 17:31:15.957Z",
+ "name": "feeds",
+ "type": "base",
+ "system": false,
+ "schema": [
+ {
+ "system": false,
+ "id": "cowxjfmc",
+ "name": "modules",
+ "type": "json",
+ "required": true,
+ "unique": false,
+ "options": {}
+ }
+ ],
+ "indexes": [],
+ "listRule": null,
+ "viewRule": null,
+ "createRule": null,
+ "updateRule": null,
+ "deleteRule": null,
+ "options": {}
+ },
+ {
+ "id": "7her4515qsmrxe8",
+ "created": "2023-09-19 17:31:15.958Z",
+ "updated": "2023-09-19 17:31:15.958Z",
+ "name": "events",
+ "type": "base",
+ "system": false,
+ "schema": [
+ {
+ "system": false,
+ "id": "m8ne8e3m",
+ "name": "Day",
+ "type": "text",
+ "required": false,
+ "unique": false,
+ "options": {
+ "min": null,
+ "max": null,
+ "pattern": ""
+ }
+ },
+ {
+ "system": false,
+ "id": "xnsxqp7j",
+ "name": "Week",
+ "type": "text",
+ "required": false,
+ "unique": false,
+ "options": {
+ "min": null,
+ "max": null,
+ "pattern": ""
+ }
+ },
+ {
+ "system": false,
+ "id": "7vsr9h6p",
+ "name": "Start",
+ "type": "text",
+ "required": false,
+ "unique": false,
+ "options": {
+ "min": null,
+ "max": null,
+ "pattern": ""
+ }
+ },
+ {
+ "system": false,
+ "id": "wwpokofe",
+ "name": "End",
+ "type": "text",
+ "required": false,
+ "unique": false,
+ "options": {
+ "min": null,
+ "max": null,
+ "pattern": ""
+ }
+ },
+ {
+ "system": false,
+ "id": "aeuskrjo",
+ "name": "Name",
+ "type": "text",
+ "required": false,
+ "unique": false,
+ "options": {
+ "min": null,
+ "max": null,
+ "pattern": ""
+ }
+ },
+ {
+ "system": false,
+ "id": "klrzqyw0",
+ "name": "EventType",
+ "type": "text",
+ "required": false,
+ "unique": false,
+ "options": {
+ "min": null,
+ "max": null,
+ "pattern": ""
+ }
+ },
+ {
+ "system": false,
+ "id": "5zltexoy",
+ "name": "Prof",
+ "type": "text",
+ "required": false,
+ "unique": false,
+ "options": {
+ "min": null,
+ "max": null,
+ "pattern": ""
+ }
+ },
+ {
+ "system": false,
+ "id": "gy3nvfmx",
+ "name": "Rooms",
+ "type": "text",
+ "required": false,
+ "unique": false,
+ "options": {
+ "min": null,
+ "max": null,
+ "pattern": ""
+ }
+ },
+ {
+ "system": false,
+ "id": "hn7b8dfy",
+ "name": "Notes",
+ "type": "text",
+ "required": false,
+ "unique": false,
+ "options": {
+ "min": null,
+ "max": null,
+ "pattern": ""
+ }
+ },
+ {
+ "system": false,
+ "id": "axskpwm8",
+ "name": "BookedAt",
+ "type": "text",
+ "required": false,
+ "unique": false,
+ "options": {
+ "min": null,
+ "max": null,
+ "pattern": ""
+ }
+ },
+ {
+ "system": false,
+ "id": "vyyefxp7",
+ "name": "course",
+ "type": "text",
+ "required": false,
+ "unique": false,
+ "options": {
+ "min": null,
+ "max": null,
+ "pattern": ""
+ }
+ },
+ {
+ "system": false,
+ "id": "vlbpm9fz",
+ "name": "semester",
+ "type": "text",
+ "required": 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": {}
+ }
+ ]`
+
+ var collections []*models.Collection
+ if err := json.Unmarshal([]byte(jsonData), &collections); err != nil {
+ return err
+ }
+
+ return daos.New(db).ImportCollections(collections, true, nil)
+ }, func(db dbx.Builder) error {
+ return nil
+ })
+}
diff --git a/backend/migrations/1695151278_add_admin_account.go b/backend/migrations/1695151278_add_admin_account.go
new file mode 100644
index 0000000..c6f6ecd
--- /dev/null
+++ b/backend/migrations/1695151278_add_admin_account.go
@@ -0,0 +1,34 @@
+package migrations
+
+import (
+ "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 {
+ dao := daos.New(db)
+
+ admin := &models.Admin{}
+ admin.Email = "demo@htwkalender.de"
+ err := admin.SetPassword("htwkalender-demo")
+ if err != nil {
+ return err
+ }
+
+ return dao.SaveAdmin(admin)
+ }, func(db dbx.Builder) error { // optional revert operation
+
+ dao := daos.New(db)
+
+ admin, _ := dao.FindAdminByEmail("test@example.com")
+ if admin != nil {
+ return dao.DeleteAdmin(admin)
+ }
+
+ // already deleted
+ return nil
+ })
+}
diff --git a/model/feedModel.go b/backend/model/feedModel.go
similarity index 100%
rename from model/feedModel.go
rename to backend/model/feedModel.go
diff --git a/model/icalModel.go b/backend/model/icalModel.go
similarity index 100%
rename from model/icalModel.go
rename to backend/model/icalModel.go
diff --git a/model/seminarGroup.go b/backend/model/seminarGroup.go
similarity index 100%
rename from model/seminarGroup.go
rename to backend/model/seminarGroup.go
diff --git a/model/seminarGroupXMLStruct.go b/backend/model/seminarGroupXMLStruct.go
similarity index 100%
rename from model/seminarGroupXMLStruct.go
rename to backend/model/seminarGroupXMLStruct.go
diff --git a/openapi.yml b/backend/openapi.yml
similarity index 100%
rename from openapi.yml
rename to backend/openapi.yml
diff --git a/pb_schema.json b/backend/pb_schema.json
similarity index 98%
rename from pb_schema.json
rename to backend/pb_schema.json
index 358691b..d3536f9 100644
--- a/pb_schema.json
+++ b/backend/pb_schema.json
@@ -144,6 +144,29 @@
"deleteRule": null,
"options": {}
},
+ {
+ "id": "d65h4wh7zk13gxp",
+ "name": "feeds",
+ "type": "base",
+ "system": false,
+ "schema": [
+ {
+ "id": "cowxjfmc",
+ "name": "modules",
+ "type": "json",
+ "system": false,
+ "required": true,
+ "options": {}
+ }
+ ],
+ "indexes": [],
+ "listRule": null,
+ "viewRule": null,
+ "createRule": null,
+ "updateRule": null,
+ "deleteRule": null,
+ "options": {}
+ },
{
"id": "7her4515qsmrxe8",
"name": "events",
@@ -296,7 +319,7 @@
}
],
"indexes": [
- "CREATE UNIQUE INDEX `idx_orp1NWL` ON `events` (\n `Day`,\n `Week`,\n `Start`,\n `End`,\n `Name`,\n `course`\n)"
+ "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,
@@ -304,28 +327,5 @@
"updateRule": null,
"deleteRule": null,
"options": {}
- },
- {
- "id": "d65h4wh7zk13gxp",
- "name": "feeds",
- "type": "base",
- "system": false,
- "schema": [
- {
- "id": "cowxjfmc",
- "name": "modules",
- "type": "json",
- "system": false,
- "required": true,
- "options": {}
- }
- ],
- "indexes": [],
- "listRule": null,
- "viewRule": null,
- "createRule": null,
- "updateRule": null,
- "deleteRule": null,
- "options": {}
}
]
\ No newline at end of file
diff --git a/addRoute.go b/backend/service/addRoute.go
similarity index 89%
rename from addRoute.go
rename to backend/service/addRoute.go
index 9510a66..94c0281 100644
--- a/addRoute.go
+++ b/backend/service/addRoute.go
@@ -1,19 +1,19 @@
-package main
+package service
import (
"github.com/labstack/echo/v5"
"github.com/pocketbase/pocketbase"
"github.com/pocketbase/pocketbase/apis"
"github.com/pocketbase/pocketbase/core"
- "htwk-planner/service/events"
- "htwk-planner/service/fetch"
+ events2 "htwk-planner/service/events"
+ fetch2 "htwk-planner/service/fetch"
"htwk-planner/service/ical"
"htwk-planner/service/room"
"net/http"
"os"
)
-func addRoutes(app *pocketbase.PocketBase) {
+func AddRoutes(app *pocketbase.PocketBase) {
app.OnBeforeServe().Add(func(e *core.ServeEvent) error {
e.Router.GET("/*", apis.StaticDirectoryHandler(os.DirFS("./pb_public"), false))
@@ -25,7 +25,7 @@ func addRoutes(app *pocketbase.PocketBase) {
Method: http.MethodGet,
Path: "/api/fetchPlans",
Handler: func(c echo.Context) error {
- return fetch.GetSeminarEvents(c, app)
+ return fetch2.GetSeminarEvents(c, app)
},
Middlewares: []echo.MiddlewareFunc{
apis.ActivityLogger(app),
@@ -42,7 +42,7 @@ func addRoutes(app *pocketbase.PocketBase) {
Method: http.MethodGet,
Path: "/api/fetchGroups",
Handler: func(c echo.Context) error {
- return fetch.SeminarGroups(c, app)
+ return fetch2.SeminarGroups(c, app)
},
Middlewares: []echo.MiddlewareFunc{
apis.ActivityLogger(app),
@@ -113,7 +113,7 @@ func addRoutes(app *pocketbase.PocketBase) {
Handler: func(c echo.Context) error {
course := c.QueryParam("course")
semester := c.QueryParam("semester")
- return events.GetModulesForCourseDistinct(app, c, course, semester)
+ return events2.GetModulesForCourseDistinct(app, c, course, semester)
},
Middlewares: []echo.MiddlewareFunc{
apis.ActivityLogger(app),
@@ -130,7 +130,7 @@ func addRoutes(app *pocketbase.PocketBase) {
Method: http.MethodGet,
Path: "/api/modules",
Handler: func(c echo.Context) error {
- return events.GetAllModulesDistinct(app, c)
+ return events2.GetAllModulesDistinct(app, c)
},
Middlewares: []echo.MiddlewareFunc{
apis.ActivityLogger(app),
@@ -147,7 +147,7 @@ func addRoutes(app *pocketbase.PocketBase) {
Method: http.MethodGet,
Path: "/api/courses",
Handler: func(c echo.Context) error {
- return events.GetAllCourses(app, c)
+ return events2.GetAllCourses(app, c)
},
Middlewares: []echo.MiddlewareFunc{
apis.ActivityLogger(app),
diff --git a/service/date/dateFormat.go b/backend/service/date/dateFormat.go
similarity index 100%
rename from service/date/dateFormat.go
rename to backend/service/date/dateFormat.go
diff --git a/service/date/dateFormat_test.go b/backend/service/date/dateFormat_test.go
similarity index 100%
rename from service/date/dateFormat_test.go
rename to backend/service/date/dateFormat_test.go
diff --git a/service/db/dbEvents.go b/backend/service/db/dbEvents.go
similarity index 68%
rename from service/db/dbEvents.go
rename to backend/service/db/dbEvents.go
index a828518..bd83640 100644
--- a/service/db/dbEvents.go
+++ b/backend/service/db/dbEvents.go
@@ -6,35 +6,78 @@ import (
"github.com/pocketbase/pocketbase"
"github.com/pocketbase/pocketbase/models"
"htwk-planner/model"
+ "log"
"strings"
"time"
)
-func SaveEvents(seminarGroup []model.SeminarGroup, collection *models.Collection, app *pocketbase.PocketBase) error {
+func SaveEvents(seminarGroup []model.SeminarGroup, collection *models.Collection, app *pocketbase.PocketBase) ([]*models.Record, error) {
+ var toBeSavedEvents []struct {
+ model.Event
+ string
+ }
+ var savedRecords []*models.Record
+ var insertRecords []*models.Record
+ // check if event is already in database and add to toBeSavedEvents if not
for _, seminarGroup := range seminarGroup {
for _, event := range seminarGroup.Events {
- var err error = nil
- record := models.NewRecord(collection)
- record.Set("Day", event.Day)
- record.Set("Week", event.Week)
- record.Set("Start", event.Start)
- record.Set("End", event.End)
- record.Set("Name", event.Name)
- record.Set("EventType", event.EventType)
- record.Set("Prof", event.Prof)
- record.Set("Rooms", event.Rooms)
- record.Set("Notes", event.Notes)
- record.Set("BookedAt", event.BookedAt)
- record.Set("course", seminarGroup.Course)
- record.Set("semester", event.Semester)
- err = app.Dao().SaveRecord(record)
- if err != nil {
- println("Error while saving record: ", err.Error())
+ dbGroup, err := findEventByDayWeekStartEndNameCourse(event, seminarGroup.Course, app)
+
+ if dbGroup == nil && err.Error() == "sql: no rows in result set" {
+ toBeSavedEvents = append(toBeSavedEvents, struct {
+ model.Event
+ string
+ }{event, seminarGroup.Course})
+ } else if err != nil {
+ return nil, err
}
}
}
- return nil
+
+ // create record for each event that's not already in the database
+ for _, event := range toBeSavedEvents {
+ record := models.NewRecord(collection)
+ record.Set("Day", event.Day)
+ record.Set("Week", event.Week)
+ record.Set("Start", event.Start)
+ record.Set("End", event.End)
+ record.Set("Name", event.Name)
+ record.Set("EventType", event.EventType)
+ record.Set("Prof", event.Prof)
+ record.Set("Rooms", event.Rooms)
+ record.Set("Notes", event.Notes)
+ record.Set("BookedAt", event.BookedAt)
+ record.Set("course", event.string)
+ record.Set("semester", event.Semester)
+ insertRecords = append(insertRecords, record)
+ }
+
+ // save all records
+ for _, record := range insertRecords {
+ if record != nil {
+ err := app.Dao().SaveRecord(record)
+ if err == nil {
+ savedRecords = append(savedRecords, record)
+ } else {
+ log.Println("Error while saving record: ", err)
+ return nil, err
+ }
+ }
+ }
+
+ return savedRecords, nil
+}
+
+func findEventByDayWeekStartEndNameCourse(event model.Event, course string, app *pocketbase.PocketBase) (*model.Event, error) {
+ err := app.Dao().DB().Select("*").From("events").Where(
+ dbx.NewExp("Day = {:day} AND Week = {:week} AND Start = {:start} AND End = {:end} AND Name = {:name} AND course = {:course} AND Prof = {:prof} AND Rooms = {:rooms} AND EventType = {:eventType}",
+ dbx.Params{"day": event.Day, "week": event.Week, "start": event.Start, "end": event.End, "name": event.Name, "course": course, "prof": event.Prof, "rooms": event.Rooms, "eventType": event.EventType}),
+ ).One(&event)
+ if err != nil {
+ return nil, err
+ }
+ return &event, err
}
func contains(s []string, e string) bool {
diff --git a/service/db/dbFeeds.go b/backend/service/db/dbFeeds.go
similarity index 100%
rename from service/db/dbFeeds.go
rename to backend/service/db/dbFeeds.go
diff --git a/service/db/dbFunctions.go b/backend/service/db/dbFunctions.go
similarity index 100%
rename from service/db/dbFunctions.go
rename to backend/service/db/dbFunctions.go
diff --git a/backend/service/db/dbGroups.go b/backend/service/db/dbGroups.go
new file mode 100644
index 0000000..f09c891
--- /dev/null
+++ b/backend/service/db/dbGroups.go
@@ -0,0 +1,81 @@
+package db
+
+import (
+ "github.com/pocketbase/dbx"
+ "github.com/pocketbase/pocketbase"
+ "github.com/pocketbase/pocketbase/models"
+ "htwk-planner/model"
+)
+
+func SaveGroups(seminarGroup []model.SeminarGroup, collection *models.Collection, app *pocketbase.PocketBase) ([]*models.Record, error) {
+ var savedRecords []*models.Record
+ var tobeSavedGroups []model.SeminarGroup
+ var insertRecords []*models.Record
+
+ for _, group := range seminarGroup {
+ dbGroup, err := FindGroupByCourse(group.Course, app)
+
+ if dbGroup == nil && err.Error() == "sql: no rows in result set" {
+ tobeSavedGroups = append(tobeSavedGroups, group)
+ } else if err != nil {
+ return nil, err
+ }
+ }
+
+ // create record for each group that's not already in the database
+ for _, group := range tobeSavedGroups {
+ record := models.NewRecord(collection)
+ record.Set("university", group.University)
+ record.Set("shortcut", group.GroupShortcut)
+ record.Set("groupId", group.GroupId)
+ record.Set("course", group.Course)
+ record.Set("faculty", group.Faculty)
+ record.Set("facultyId", group.FacultyId)
+ insertRecords = append(insertRecords, record)
+ }
+
+ // save all records
+ for _, record := range insertRecords {
+ if record != nil {
+ err := app.Dao().SaveRecord(record)
+ if err == nil {
+ savedRecords = append(savedRecords, record)
+ } else {
+ return nil, err
+ }
+ }
+ }
+
+ return savedRecords, nil
+}
+
+func FindGroupByCourse(course string, app *pocketbase.PocketBase) (*model.SeminarGroup, error) {
+ var group model.SeminarGroup
+ err := app.Dao().DB().Select("*").From("groups").Where(dbx.NewExp("course = {:course}", dbx.Params{"course": course})).One(&group)
+ if err != nil {
+ return nil, err
+ }
+ return &group, err
+}
+
+func GetAllCourses(app *pocketbase.PocketBase) []string {
+
+ var courses []struct {
+ CourseShortcut string `db:"course" json:"course"`
+ }
+
+ // get all rooms from event records in the events collection
+ err := app.Dao().DB().Select("course").From("groups").All(&courses)
+ if err != nil {
+ print("Error while getting groups from database: ", err)
+ return nil
+ }
+
+ var courseArray []string
+
+ for _, course := range courses {
+ courseArray = append(courseArray, course.CourseShortcut)
+ }
+
+ return courseArray
+}
diff --git a/service/events/courseService.go b/backend/service/events/courseService.go
similarity index 100%
rename from service/events/courseService.go
rename to backend/service/events/courseService.go
diff --git a/service/events/eventService.go b/backend/service/events/eventService.go
similarity index 100%
rename from service/events/eventService.go
rename to backend/service/events/eventService.go
diff --git a/service/fetch/fetchSeminarEventService.go b/backend/service/fetch/fetchSeminarEventService.go
similarity index 92%
rename from service/fetch/fetchSeminarEventService.go
rename to backend/service/fetch/fetchSeminarEventService.go
index 9cad524..6c0569e 100644
--- a/service/fetch/fetchSeminarEventService.go
+++ b/backend/service/fetch/fetchSeminarEventService.go
@@ -28,12 +28,25 @@ func GetSeminarEvents(c echo.Context, app *pocketbase.PocketBase) error {
return apis.NewNotFoundError("Collection not found", dbError)
}
- dbError = db.SaveEvents(seminarGroups, collection, app)
+ seminarGroups = clearEmptySeminarGroups(seminarGroups)
+
+ savedRecords, dbError := db.SaveEvents(seminarGroups, collection, app)
+
if dbError != nil {
- return apis.NewApiError(400, "Could not save Event into database", dbError)
+ return apis.NewNotFoundError("Events could not be saved", dbError)
}
- return c.JSON(http.StatusOK, seminarGroups)
+ return c.JSON(http.StatusOK, savedRecords)
+}
+
+func clearEmptySeminarGroups(seminarGroups []model.SeminarGroup) []model.SeminarGroup {
+ var newSeminarGroups []model.SeminarGroup
+ for _, seminarGroup := range seminarGroups {
+ if len(seminarGroup.Events) > 0 && seminarGroup.Course != "" {
+ newSeminarGroups = append(newSeminarGroups, seminarGroup)
+ }
+ }
+ return newSeminarGroups
}
func GetSeminarGroupsEventsFromHTML(seminarGroupsLabel []string) []model.SeminarGroup {
diff --git a/service/fetch/fetchSeminarEventService_test.go b/backend/service/fetch/fetchSeminarEventService_test.go
similarity index 100%
rename from service/fetch/fetchSeminarEventService_test.go
rename to backend/service/fetch/fetchSeminarEventService_test.go
diff --git a/service/fetch/fetchSeminarGroupService.go b/backend/service/fetch/fetchSeminarGroupService.go
similarity index 90%
rename from service/fetch/fetchSeminarGroupService.go
rename to backend/service/fetch/fetchSeminarGroupService.go
index fa180ca..53a64ee 100644
--- a/service/fetch/fetchSeminarGroupService.go
+++ b/backend/service/fetch/fetchSeminarGroupService.go
@@ -6,6 +6,7 @@ import (
"github.com/labstack/echo/v5"
"github.com/pocketbase/pocketbase"
"github.com/pocketbase/pocketbase/apis"
+ "github.com/pocketbase/pocketbase/models"
"htwk-planner/model"
"htwk-planner/service/db"
"io"
@@ -56,13 +57,14 @@ func SeminarGroups(c echo.Context, app *pocketbase.PocketBase) error {
if dbError != nil {
return apis.NewNotFoundError("Collection not found", dbError)
}
+ var insertedGroups []*models.Record
- dbError = db.SaveGroups(groups, collection, app)
+ insertedGroups, dbError = db.SaveGroups(groups, collection, app)
if dbError != nil {
- return apis.NewApiError(400, "Could not save Event into database", dbError)
+ return apis.NewNotFoundError("Records could not be saved", dbError)
}
- return c.JSON(http.StatusOK, groups)
+ return c.JSON(http.StatusOK, insertedGroups)
}
func removeDuplicates(groups []model.SeminarGroup) []model.SeminarGroup {
diff --git a/service/fetch/htmlParsingFunctions.go b/backend/service/fetch/htmlParsingFunctions.go
similarity index 100%
rename from service/fetch/htmlParsingFunctions.go
rename to backend/service/fetch/htmlParsingFunctions.go
diff --git a/service/ical/ical.go b/backend/service/ical/ical.go
similarity index 97%
rename from service/ical/ical.go
rename to backend/service/ical/ical.go
index 2bcfe4b..132ea2f 100644
--- a/service/ical/ical.go
+++ b/backend/service/ical/ical.go
@@ -7,8 +7,8 @@ import (
"github.com/labstack/echo/v5"
"github.com/pocketbase/pocketbase"
"github.com/pocketbase/pocketbase/apis"
- "htwk-planner/model"
- "htwk-planner/service/db"
+ model "htwk-planner/model"
+ db "htwk-planner/service/db"
"io"
"net/http"
"time"
diff --git a/service/room/roomService.go b/backend/service/room/roomService.go
similarity index 100%
rename from service/room/roomService.go
rename to backend/service/room/roomService.go
diff --git a/docker-compose.yml b/docker-compose.yml
new file mode 100644
index 0000000..8e105d5
--- /dev/null
+++ b/docker-compose.yml
@@ -0,0 +1,33 @@
+version: "3.9"
+
+services:
+ htwkalender-backend:
+ build:
+ dockerfile: Dockerfile
+ context: ./backend
+ # open port 8090
+ ports:
+ - "8090:8090"
+ command: "/htwkalender serve --http=0.0.0.0:8090 --dir=/pb_data"
+ volumes:
+ - ./backend/pb_data:/pb_data
+ htwkalender-frontend:
+ volumes:
+ - ./frontend/src:/app/src
+ build:
+ dockerfile: Dockerfile
+ context: ./frontend
+ # open port 8000
+ ports:
+ - "8000:8000"
+ command: "npm run dev"
+
+ rproxy:
+ image: nginx:stable
+ volumes:
+ - ./reverseproxy.conf:/etc/nginx/nginx.conf
+ depends_on:
+ - htwkalender-backend
+ - htwkalender-frontend
+ ports:
+ - "80:80"
diff --git a/frontend/.dockerignore b/frontend/.dockerignore
new file mode 100644
index 0000000..b512c09
--- /dev/null
+++ b/frontend/.dockerignore
@@ -0,0 +1 @@
+node_modules
\ No newline at end of file
diff --git a/frontend/.eslintrc.cjs b/frontend/.eslintrc.cjs
new file mode 100644
index 0000000..0d380b3
--- /dev/null
+++ b/frontend/.eslintrc.cjs
@@ -0,0 +1,14 @@
+module.exports = {
+ root: true,
+ env: {
+ node: true,
+ },
+ extends: [
+ "eslint:recommended",
+ "plugin:vue/vue3-recommended",
+ "prettier",
+ "@vue/typescript/recommended",
+ ],
+ parserOptions: {},
+ rules: {},
+};
diff --git a/frontend/.gitignore b/frontend/.gitignore
new file mode 100644
index 0000000..a547bf3
--- /dev/null
+++ b/frontend/.gitignore
@@ -0,0 +1,24 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
+node_modules
+dist
+dist-ssr
+*.local
+
+# Editor directories and files
+.vscode/*
+!.vscode/extensions.json
+.idea
+.DS_Store
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?
diff --git a/frontend/.prettierrc.json b/frontend/.prettierrc.json
new file mode 100644
index 0000000..0967ef4
--- /dev/null
+++ b/frontend/.prettierrc.json
@@ -0,0 +1 @@
+{}
diff --git a/frontend/Dockerfile b/frontend/Dockerfile
new file mode 100644
index 0000000..937d52a
--- /dev/null
+++ b/frontend/Dockerfile
@@ -0,0 +1,7 @@
+FROM node:latest
+
+WORKDIR /app
+COPY package*.json ./
+RUN npm install
+COPY ./ ./
+
diff --git a/frontend/README.md b/frontend/README.md
new file mode 100644
index 0000000..66d9079
--- /dev/null
+++ b/frontend/README.md
@@ -0,0 +1,20 @@
+# Vue 3 + TypeScript + Vite
+
+This template should help get you started developing with Vue 3 and TypeScript in Vite. The template uses Vue 3 `
-
-
+