mirror of
https://gitlab.dit.htwk-leipzig.de/htwk-software/htwkalender-pwa.git
synced 2026-01-16 11:12:25 +01:00
add first ical implementation
This commit is contained in:
47
service/date/dateFormat.go
Normal file
47
service/date/dateFormat.go
Normal file
@@ -0,0 +1,47 @@
|
||||
package date
|
||||
|
||||
import "time"
|
||||
|
||||
func GetDateFromWeekNumber(year int, weekNumber int, dayName string) (time.Time, error) {
|
||||
// Create a time.Date for the first day of the year
|
||||
firstDayOfYear := time.Date(year, time.January, 1, 0, 0, 0, 0, time.UTC)
|
||||
|
||||
// Calculate the number of days to add to reach the desired week
|
||||
daysToAdd := time.Duration((weekNumber-1)*7) * 24 * time.Hour
|
||||
|
||||
// Find the starting day of the week (e.g., Monday)
|
||||
startingDayOfWeek := firstDayOfYear
|
||||
|
||||
// check if the first day of the year is friday or saturday or sunday
|
||||
|
||||
if startingDayOfWeek.Weekday() == time.Friday || startingDayOfWeek.Weekday() == time.Saturday || startingDayOfWeek.Weekday() == time.Sunday {
|
||||
for startingDayOfWeek.Weekday() != time.Monday {
|
||||
startingDayOfWeek = startingDayOfWeek.Add(24 * time.Hour)
|
||||
}
|
||||
} else {
|
||||
for startingDayOfWeek.Weekday() != time.Monday {
|
||||
startingDayOfWeek = startingDayOfWeek.Add(-24 * time.Hour)
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate the desired date by adding daysToAdd and adjusting for the day name
|
||||
desiredDate := startingDayOfWeek.Add(daysToAdd)
|
||||
|
||||
// Find the day of the week
|
||||
dayOfWeek := map[string]time.Weekday{
|
||||
"Montag": time.Monday,
|
||||
"Dienstag": time.Tuesday,
|
||||
"Mittwoch": time.Wednesday,
|
||||
"Donnerstag": time.Thursday,
|
||||
"Freitag": time.Friday,
|
||||
"Samstag": time.Saturday,
|
||||
"Sonntag": time.Sunday,
|
||||
}[dayName]
|
||||
|
||||
// Adjust to the desired day of the week
|
||||
for desiredDate.Weekday() != dayOfWeek {
|
||||
desiredDate = desiredDate.Add(24 * time.Hour)
|
||||
}
|
||||
|
||||
return desiredDate, nil
|
||||
}
|
||||
64
service/date/dateFormat_test.go
Normal file
64
service/date/dateFormat_test.go
Normal file
@@ -0,0 +1,64 @@
|
||||
package date
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func Test_getDateFromWeekNumber(t *testing.T) {
|
||||
type args struct {
|
||||
year int
|
||||
weekNumber int
|
||||
dayName string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want time.Time
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "Test 1",
|
||||
args: args{
|
||||
year: 2021,
|
||||
weekNumber: 1,
|
||||
dayName: "Montag",
|
||||
},
|
||||
want: time.Date(2021, 1, 4, 0, 0, 0, 0, time.UTC),
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "Test 2",
|
||||
args: args{
|
||||
year: 2023,
|
||||
weekNumber: 57,
|
||||
dayName: "Montag",
|
||||
},
|
||||
want: time.Date(2024, 1, 29, 0, 0, 0, 0, time.UTC),
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "Test 3",
|
||||
args: args{
|
||||
year: 2023,
|
||||
weekNumber: 1,
|
||||
dayName: "Montag",
|
||||
},
|
||||
want: time.Date(2023, 1, 2, 0, 0, 0, 0, time.UTC),
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := GetDateFromWeekNumber(tt.args.year, tt.args.weekNumber, tt.args.dayName)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("getDateFromWeekNumber() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("getDateFromWeekNumber() got = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,13 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"github.com/jordic/goics"
|
||||
"github.com/pocketbase/dbx"
|
||||
"github.com/pocketbase/pocketbase"
|
||||
"github.com/pocketbase/pocketbase/models"
|
||||
"htwk-planner/model"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
func SaveEvents(seminarGroup []model.SeminarGroup, collection *models.Collection, app *pocketbase.PocketBase) error {
|
||||
@@ -24,6 +27,7 @@ func SaveEvents(seminarGroup []model.SeminarGroup, collection *models.Collection
|
||||
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())
|
||||
@@ -42,7 +46,7 @@ func contains(s []string, e string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// GetRooms function to get all rooms from database that are stored as a string in the Events struct
|
||||
// GetRooms function to get all rooms from database that are stored as a string in the Event struct
|
||||
func GetRooms(app *pocketbase.PocketBase) []string {
|
||||
|
||||
var events []struct {
|
||||
@@ -70,3 +74,39 @@ func GetRooms(app *pocketbase.PocketBase) []string {
|
||||
}
|
||||
return roomArray
|
||||
}
|
||||
|
||||
type Events []*model.Event
|
||||
|
||||
// EmitICal implements the interface for goics
|
||||
func (e Events) EmitICal() goics.Componenter {
|
||||
c := goics.NewComponent()
|
||||
c.SetType("VCALENDAR")
|
||||
c.AddProperty("CALSCAL", "GREGORIAN")
|
||||
for _, event := range e {
|
||||
s := goics.NewComponent()
|
||||
s.SetType("VEVENT")
|
||||
timeEnd, _ := time.Parse("2024-01-07 07:00:00 +0000 UTC", event.End)
|
||||
timeStart, _ := time.Parse("2024-01-07 07:00:00 +0000 UTC", event.Start)
|
||||
k, v := goics.FormatDateTimeField("DTEND", timeEnd)
|
||||
s.AddProperty(k, v)
|
||||
k, v = goics.FormatDateTimeField("DTSTART", timeStart)
|
||||
s.AddProperty(k, v)
|
||||
s.AddProperty("SUMMARY", event.Name)
|
||||
s.AddProperty("DESCRIPTION", event.Notes)
|
||||
s.AddProperty("LOCATION", event.Rooms)
|
||||
c.AddComponent(s)
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
// GetPlanForCourseAndSemester gets all events for specific course and semester
|
||||
func GetPlanForCourseAndSemester(app *pocketbase.PocketBase, course string, semester string) Events {
|
||||
var events Events
|
||||
// get all events from event records in the events collection
|
||||
err := app.Dao().DB().Select("*").From("events").Where(dbx.NewExp("course = {:course} AND semester = {:semester}", dbx.Params{"course": course, "semester": semester})).All(&events)
|
||||
if err != nil {
|
||||
print("Error while getting events from database: ", err)
|
||||
return nil
|
||||
}
|
||||
return events
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package service
|
||||
package fetch
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
@@ -40,7 +40,7 @@ func getSeminarHTML() (string, error) {
|
||||
|
||||
}
|
||||
|
||||
func FetchSeminarGroups(c echo.Context, app *pocketbase.PocketBase) error {
|
||||
func SeminarGroups(c echo.Context, app *pocketbase.PocketBase) error {
|
||||
|
||||
result, _ := getSeminarHTML()
|
||||
|
||||
@@ -55,7 +55,7 @@ func FetchSeminarGroups(c echo.Context, app *pocketbase.PocketBase) error {
|
||||
|
||||
dbError = db.SaveGroups(groups, collection, app)
|
||||
if dbError != nil {
|
||||
return apis.NewApiError(400, "Could not save Events into database", dbError)
|
||||
return apis.NewApiError(400, "Could not save Event into database", dbError)
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusOK, groups)
|
||||
@@ -64,7 +64,6 @@ func FetchSeminarGroups(c echo.Context, app *pocketbase.PocketBase) error {
|
||||
func parseSeminarGroups(result string) []model.SeminarGroup {
|
||||
|
||||
var studium model.Studium
|
||||
|
||||
err := xml.Unmarshal([]byte(result), &studium)
|
||||
if err != nil {
|
||||
return nil
|
||||
@@ -1,4 +1,4 @@
|
||||
package service
|
||||
package fetch
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
@@ -8,11 +8,14 @@ import (
|
||||
"github.com/pocketbase/pocketbase/models"
|
||||
"golang.org/x/net/html"
|
||||
"htwk-planner/model"
|
||||
"htwk-planner/service/date"
|
||||
"htwk-planner/service/db"
|
||||
"io"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
func FetchHTWK(c echo.Context, app *pocketbase.PocketBase) error {
|
||||
@@ -44,7 +47,7 @@ func FetchHTWK(c echo.Context, app *pocketbase.PocketBase) error {
|
||||
|
||||
dbError = db.SaveEvents(seminarGroups, collection, app)
|
||||
if dbError != nil {
|
||||
return apis.NewApiError(400, "Could not save Events into database", dbError)
|
||||
return apis.NewApiError(400, "Could not save Event into database", dbError)
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusOK, seminarGroups)
|
||||
@@ -73,24 +76,87 @@ func parseSeminarGroup(result string) model.SeminarGroup {
|
||||
eventsWithCombinedWeeks := toEvents(eventTables, allDayLabels)
|
||||
splitEventsByWeekVal := splitEventsByWeek(eventsWithCombinedWeeks)
|
||||
events := splitEventsBySingleWeek(splitEventsByWeekVal)
|
||||
semesterString := findFirstSpanWithClass(table, "header-0-2-0").FirstChild.Data
|
||||
semester, year := extractSemesterAndYear(semesterString)
|
||||
events = convertWeeksToDates(events, semester, year)
|
||||
var seminarGroup = model.SeminarGroup{
|
||||
University: findFirstSpanWithClass(table, "header-1-0-0").FirstChild.Data,
|
||||
Course: findFirstSpanWithClass(table, "header-2-0-1").FirstChild.Data,
|
||||
Events: events,
|
||||
}
|
||||
|
||||
return seminarGroup
|
||||
}
|
||||
|
||||
func toEvents(tables [][]*html.Node, days []string) []model.Events {
|
||||
var events []model.Events
|
||||
func convertWeeksToDates(events []model.Event, semester string, year string) []model.Event {
|
||||
var newEvents []model.Event
|
||||
eventYear, _ := strconv.Atoi(year)
|
||||
|
||||
// for each event we need to calculate the start and end date based on the week and the year
|
||||
for _, event := range events {
|
||||
eventWeek, _ := strconv.Atoi(event.Week)
|
||||
eventDay, _ := date.GetDateFromWeekNumber(eventYear, eventWeek, event.Day)
|
||||
start := addTimeToDate(eventDay, event.Start)
|
||||
end := addTimeToDate(eventDay, event.End)
|
||||
newEvent := event
|
||||
newEvent.Start = start.String()
|
||||
newEvent.End = end.String()
|
||||
newEvent.Semester = semester
|
||||
newEvents = append(newEvents, newEvent)
|
||||
}
|
||||
return newEvents
|
||||
}
|
||||
|
||||
func addTimeToDate(date time.Time, timeString string) time.Time {
|
||||
//convert time string to time
|
||||
timeParts := strings.Split(timeString, ":")
|
||||
hour, _ := strconv.Atoi(timeParts[0])
|
||||
minute, _ := strconv.Atoi(timeParts[1])
|
||||
|
||||
return time.Date(date.Year(), date.Month(), date.Day(), hour, minute, 0, 0, time.UTC)
|
||||
}
|
||||
|
||||
func extractSemesterAndYear(semesterString string) (string, string) {
|
||||
winterPattern := "Wintersemester"
|
||||
summerPattern := "Sommersemester"
|
||||
|
||||
winterMatch := strings.Contains(semesterString, winterPattern)
|
||||
summerMatch := strings.Contains(semesterString, summerPattern)
|
||||
|
||||
semester := ""
|
||||
semesterShortcut := ""
|
||||
|
||||
if winterMatch {
|
||||
semester = "Wintersemester"
|
||||
semesterShortcut = "ws"
|
||||
} else if summerMatch {
|
||||
semester = "Sommersemester"
|
||||
semesterShortcut = "ss"
|
||||
} else {
|
||||
return "", ""
|
||||
}
|
||||
|
||||
yearPattern := `\d{4}`
|
||||
combinedPattern := semester + `\s` + yearPattern
|
||||
re := regexp.MustCompile(combinedPattern)
|
||||
match := re.FindString(semesterString)
|
||||
year := ""
|
||||
|
||||
if match != "" {
|
||||
reYear := regexp.MustCompile(yearPattern)
|
||||
year = reYear.FindString(match)
|
||||
}
|
||||
return semesterShortcut, year
|
||||
}
|
||||
|
||||
func toEvents(tables [][]*html.Node, days []string) []model.Event {
|
||||
var events []model.Event
|
||||
|
||||
for table := range tables {
|
||||
for row := range tables[table] {
|
||||
|
||||
tableData := findTableData(tables[table][row])
|
||||
if len(tableData) > 0 {
|
||||
events = append(events, model.Events{
|
||||
events = append(events, model.Event{
|
||||
Day: days[table],
|
||||
Week: getTextContent(tableData[0]),
|
||||
Start: getTextContent(tableData[1]),
|
||||
@@ -110,8 +176,8 @@ func toEvents(tables [][]*html.Node, days []string) []model.Events {
|
||||
return events
|
||||
}
|
||||
|
||||
func splitEventsByWeek(events []model.Events) []model.Events {
|
||||
var newEvents []model.Events
|
||||
func splitEventsByWeek(events []model.Event) []model.Event {
|
||||
var newEvents []model.Event
|
||||
|
||||
for _, event := range events {
|
||||
weeks := strings.Split(event.Week, ",")
|
||||
@@ -124,8 +190,8 @@ func splitEventsByWeek(events []model.Events) []model.Events {
|
||||
return newEvents
|
||||
}
|
||||
|
||||
func splitEventsBySingleWeek(events []model.Events) []model.Events {
|
||||
var newEvents []model.Events
|
||||
func splitEventsBySingleWeek(events []model.Event) []model.Event {
|
||||
var newEvents []model.Event
|
||||
|
||||
for _, event := range events {
|
||||
if strings.Contains(event.Week, "-") {
|
||||
52
service/fetch/fetchService_test.go
Normal file
52
service/fetch/fetchService_test.go
Normal file
@@ -0,0 +1,52 @@
|
||||
package fetch
|
||||
|
||||
import "testing"
|
||||
|
||||
func Test_extractSemesterAndYear(t *testing.T) {
|
||||
type args struct {
|
||||
semesterString string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want string
|
||||
want1 string
|
||||
}{
|
||||
// TODO: Add test cases.
|
||||
{
|
||||
name: "Test 1",
|
||||
args: args{
|
||||
semesterString: "Wintersemester 2023/24 (Planungszeitraum 01.09.2023 bis 03.03.2024)",
|
||||
},
|
||||
want: "ws",
|
||||
want1: "2023",
|
||||
},
|
||||
{
|
||||
name: "Test 2",
|
||||
args: args{
|
||||
semesterString: "Sommersemester 2023 (Planungszeitraum 06.03. bis 31.08.2023)",
|
||||
},
|
||||
want: "ss",
|
||||
want1: "2023",
|
||||
},
|
||||
{
|
||||
name: "Test 3",
|
||||
args: args{
|
||||
semesterString: "Sommersemester 2010 (Planungszeitraum 06.03. bis 31.08.2023)",
|
||||
},
|
||||
want: "ss",
|
||||
want1: "2010",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, got1 := extractSemesterAndYear(tt.args.semesterString)
|
||||
if got != tt.want {
|
||||
t.Errorf("extractSemesterAndYear() got = %v, want %v", got, tt.want)
|
||||
}
|
||||
if got1 != tt.want1 {
|
||||
t.Errorf("extractSemesterAndYear() got1 = %v, want %v", got1, tt.want1)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
79
service/ical/ical.go
Normal file
79
service/ical/ical.go
Normal file
@@ -0,0 +1,79 @@
|
||||
package ical
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/rand"
|
||||
"fmt"
|
||||
"github.com/jordic/goics"
|
||||
"github.com/labstack/echo/v5"
|
||||
"github.com/pocketbase/pocketbase"
|
||||
"htwk-planner/service/db"
|
||||
"log"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
const expirationTime = 5 * time.Minute
|
||||
|
||||
var cache = make(map[string]*FeedModel)
|
||||
|
||||
func FeedURL(c echo.Context, app *pocketbase.PocketBase) error {
|
||||
token := randomToken(20)
|
||||
_, err := createFeedForToken(app, token)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return c.JSON(http.StatusOK, fmt.Sprintf("FeedToken: %s", token))
|
||||
}
|
||||
|
||||
func Feed(c echo.Context, app *pocketbase.PocketBase) error {
|
||||
|
||||
var result string
|
||||
var responseWriter http.ResponseWriter
|
||||
token := c.Request().Header.Get("token")
|
||||
log.Print("iCal feed Token: " + token)
|
||||
feed, ok := cache[token]
|
||||
if !ok || feed == nil {
|
||||
return c.JSON(http.StatusNotFound, "No FeedModel for this Token")
|
||||
}
|
||||
|
||||
result = feed.Content
|
||||
if feed.ExpiresAt.Before(time.Now()) {
|
||||
newFeed, err := createFeedForToken(app, token)
|
||||
if err != nil {
|
||||
return c.JSON(http.StatusInternalServerError, err)
|
||||
}
|
||||
result = newFeed.Content
|
||||
}
|
||||
|
||||
responseWriter.Header().Set("Content-type", "text/calendar")
|
||||
responseWriter.Header().Set("charset", "utf-8")
|
||||
responseWriter.Header().Set("Content-Disposition", "inline")
|
||||
responseWriter.Header().Set("filename", "calendar.ics")
|
||||
writeSuccess(result, responseWriter)
|
||||
c.Response().Writer = responseWriter
|
||||
return nil
|
||||
}
|
||||
|
||||
func createFeedForToken(app *pocketbase.PocketBase, token string) (*FeedModel, error) {
|
||||
res := db.GetPlanForCourseAndSemester(app, "22INM", "ws")
|
||||
b := bytes.Buffer{}
|
||||
goics.NewICalEncode(&b).Encode(res)
|
||||
feed := &FeedModel{Content: b.String(), ExpiresAt: time.Now().Add(expirationTime)}
|
||||
cache[token] = feed
|
||||
return feed, nil
|
||||
}
|
||||
|
||||
func randomToken(len int) string {
|
||||
b := make([]byte, len)
|
||||
read, err := rand.Read(b)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return fmt.Sprintf("%x", read)
|
||||
}
|
||||
|
||||
func writeSuccess(message string, w http.ResponseWriter) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte(message))
|
||||
}
|
||||
24
service/ical/icalModel.go
Normal file
24
service/ical/icalModel.go
Normal file
@@ -0,0 +1,24 @@
|
||||
package ical
|
||||
|
||||
import (
|
||||
"htwk-planner/model"
|
||||
"time"
|
||||
)
|
||||
|
||||
// FeedModel is an iCal feed
|
||||
type FeedModel struct {
|
||||
Content string
|
||||
ExpiresAt time.Time
|
||||
}
|
||||
|
||||
// Entry is a time entry
|
||||
type Entry struct {
|
||||
DateStart time.Time `json:"dateStart"`
|
||||
DateEnd time.Time `json:"dateEnd"`
|
||||
Description string `json:"description"`
|
||||
}
|
||||
|
||||
// Entries is a collection of entries
|
||||
type Entries []*Entry
|
||||
|
||||
type Events []*model.Event
|
||||
Reference in New Issue
Block a user