add first ical implementation

This commit is contained in:
Elmar Kresse
2023-08-24 19:53:46 +02:00
parent 0686b9397f
commit 075bf3c899
13 changed files with 447 additions and 19 deletions

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

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

View File

@@ -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
}

View File

@@ -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

View File

@@ -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, "-") {

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