Merge pull request #41 from HTWK-Leipzig/21-customizable-events

21 Customizable event reminders
This commit is contained in:
masterElmar
2023-11-01 21:39:01 +01:00
committed by GitHub
10 changed files with 163 additions and 39 deletions

1
.gitignore vendored
View File

@@ -1,3 +1,4 @@
.idea/
.vscode/
.DS_Store

View File

@@ -27,4 +27,5 @@ type FeedCollection struct {
Name string `db:"Name" json:"name"`
Course string `db:"course" json:"course"`
UserDefinedName string `db:"userDefinedName" json:"userDefinedName"`
Reminder bool `db:"reminder" json:"reminder"`
}

View File

@@ -118,18 +118,23 @@ func buildIcalQueryForModules(modules []model.FeedCollection) dbx.Expression {
// GetPlanForModules returns all events for the given modules with the given course
// used for the ical feed
func GetPlanForModules(app *pocketbase.PocketBase, modules []model.FeedCollection) model.Events {
func GetPlanForModules(app *pocketbase.PocketBase, modules map[string]model.FeedCollection) model.Events {
var events model.Events
modulesArray := make([]model.FeedCollection, 0, len(modules))
for _, value := range modules {
modulesArray = append(modulesArray, value)
}
// iterate over modules in 100 batch sizes
for i := 0; i < len(modules); i += 100 {
var moduleBatch []model.FeedCollection
if i+100 > len(modules) {
moduleBatch = modules[i:]
moduleBatch = modulesArray[i:]
} else {
moduleBatch = modules[i : i+100]
moduleBatch = modulesArray[i : i+100]
}
var selectedModulesQuery = buildIcalQueryForModules(moduleBatch)

View File

@@ -3,14 +3,15 @@ package ical
import (
"bytes"
"encoding/json"
"github.com/jordic/goics"
"github.com/labstack/echo/v5"
"github.com/pocketbase/pocketbase"
"github.com/pocketbase/pocketbase/apis"
"htwkalender/model"
"htwkalender/service/db"
"net/http"
"time"
"github.com/jordic/goics"
"github.com/labstack/echo/v5"
"github.com/pocketbase/pocketbase"
"github.com/pocketbase/pocketbase/apis"
)
const expirationTime = 5 * time.Minute
@@ -23,8 +24,13 @@ func Feed(c echo.Context, app *pocketbase.PocketBase, token string) error {
return c.JSON(http.StatusNotFound, err)
}
var modules []model.FeedCollection
_ = json.Unmarshal([]byte(feed.Modules), &modules)
modules := make(map[string]model.FeedCollection)
var modulesArray []model.FeedCollection
_ = json.Unmarshal([]byte(feed.Modules), &modulesArray)
for _, module := range modulesArray {
modules[module.UUID] = module
}
newFeed, err := createFeedForToken(app, modules)
if err != nil {
@@ -41,7 +47,7 @@ func Feed(c echo.Context, app *pocketbase.PocketBase, token string) error {
return nil
}
func createFeedForToken(app *pocketbase.PocketBase, modules []model.FeedCollection) (*model.FeedModel, error) {
func createFeedForToken(app *pocketbase.PocketBase, modules map[string]model.FeedCollection) (*model.FeedModel, error) {
res := db.GetPlanForModules(app, modules)
b := bytes.Buffer{}
goics.NewICalEncode(&b).Encode(IcalModel{Events: res, Mapping: modules})

View File

@@ -12,7 +12,7 @@ import (
// IcalModel local type for EmitICal function
type IcalModel struct {
Events model.Events
Mapping []model.FeedCollection
Mapping map[string]model.FeedCollection
}
// EmitICal implements the interface for goics
@@ -27,13 +27,22 @@ func (icalModel IcalModel) EmitICal() goics.Componenter {
c.AddProperty("X-WR-TIMEZONE", "Europe/Berlin")
c.AddProperty("X-LIC-LOCATION", "Europe/Berlin")
for _, event := range icalModel.Events {
mapEntry, mappingFound := icalModel.Mapping[event.UUID]
s := goics.NewComponent()
s.SetType("VEVENT")
k, v := goics.FormatDateTime("DTEND;TZID=Europe/Berlin", event.End.Time().Local().In(europeTime))
s.AddProperty(k, v)
k, v = goics.FormatDateTime("DTSTART;TZID=Europe/Berlin", event.Start.Time().Local().In(europeTime))
s.AddProperty(k, v)
addPropertyIfNotEmpty(s, "SUMMARY", replaceNameIfUserDefined(&event, icalModel.Mapping))
if mappingFound {
addPropertyIfNotEmpty(s, "SUMMARY", replaceNameIfUserDefined(&event, mapEntry))
addAlarmIfSpecified(s, event, mapEntry)
} else {
addPropertyIfNotEmpty(s, "SUMMARY", event.Name)
}
addPropertyIfNotEmpty(s, "DESCRIPTION", generateDescription(event))
addPropertyIfNotEmpty(s, "LOCATION", event.Rooms)
c.AddComponent(s)
@@ -41,14 +50,25 @@ func (icalModel IcalModel) EmitICal() goics.Componenter {
return c
}
// if reminder is specified in the configuration for this event, an alarm will be added to the event
func addAlarmIfSpecified(s *goics.Component, event model.Event, mapping model.FeedCollection) {
if mapping.Reminder {
a := goics.NewComponent()
a.SetType("VALARM")
a.AddProperty("TRIGGER", "-PT15M")
a.AddProperty("ACTION", "DISPLAY")
a.AddProperty("DESCRIPTION", "Next course: "+replaceNameIfUserDefined(&event, mapping)+" in "+event.Rooms)
s.AddComponent(a)
}
}
// replaceNameIfUserDefined replaces the name of the event with the user defined name if it is not empty
// all contained template strings will be replaced with the corresponding values from the event
func replaceNameIfUserDefined(event *model.Event, mapping []model.FeedCollection) string {
for _, mapEntry := range mapping {
if mapEntry.Name == event.Name && !functions.OnlyWhitespace(mapEntry.UserDefinedName) {
return names.ReplaceTemplateSubStrings(mapEntry.UserDefinedName, *event)
}
func replaceNameIfUserDefined(event *model.Event, mapping model.FeedCollection) string {
if !functions.OnlyWhitespace(mapping.UserDefinedName) {
return names.ReplaceTemplateSubStrings(mapping.UserDefinedName, *event)
}
return event.Name
}

View File

@@ -21,7 +21,7 @@ const placeholders = ref([
@click="helpVisible = true" />
<Dialog
v-model:visible="helpVisible"
header="Module placeholders"
header="Module configuration"
>
<p>
Here you can rename your modules to your liking. This will be the name
@@ -37,6 +37,10 @@ const placeholders = ref([
<Column field="description" header="Description"></Column>
<Column field="examples" header="Examples"></Column>
</DataTable>
<p>
Additionally, you can toggle notifications for each module.
If you do so, you will be notified 15 minutes before the event starts.
</p>
</Dialog>
</template>

View File

@@ -18,6 +18,7 @@ const tableData = ref(
const columns = ref([
{ field: "Course", header: "Course" },
{ field: "Module", header: "Module" },
{ field: "Reminder", header: "Reminder"}
]);
async function finalStep() {
@@ -30,7 +31,7 @@ async function finalStep() {
<template>
<div class="flex flex-column">
<div class="flex align-items-center justify-content-center h-4rem m-2">
<h3>Rename your selected Modules to your liking.</h3>
<h3>Configure your selected Modules to your liking.</h3>
<ModuleTemplateDialog />
</div>
<div class="card flex align-items-center justify-content-center m-2">
@@ -40,30 +41,68 @@ async function finalStep() {
table-class="editable-cells-table"
responsive-layout="scroll"
>
<template #header>
<div class="flex align-items-center justify-content-end">
Enable all notifications:
<InputSwitch
class="mx-4"
:model-value="tableData.reduce((acc, curr) => acc && curr.Module.reminder, true)"
@update:model-value="tableData.forEach((module) => module.Module.reminder = $event)"
/>
</div>
</template>
<Column
v-for="col of columns"
:key="col.field"
:field="col.field"
:header="col.header"
:class="col.field === 'Reminder' ? 'text-center' : ''"
>
<!-- Text Body -->
<template #body="{ data, field }">
<div>
{{
field === "Module" ? data[field].userDefinedName : data[field]
}}
</div>
</template>
<template #editor="{ data, field }">
<template v-if="field !== 'Module'">
<div>{{ data[field] }}</div>
<template v-if="field === 'Module'">
{{ data[field].userDefinedName }}
</template>
<template v-else-if="field === 'Reminder'" class="align-content-center">
<Button
:icon="data.Module.reminder ? 'pi pi-bell' : 'pi pi-times'"
:severity="data.Module.reminder ? 'warning' : 'secondary'"
rounded
outlined
class="small-button"
@click = "data.Module.reminder = !data.Module.reminder"
></Button>
</template>
<template v-else>
{{ data[field] }}
</template>
</template>
<!-- Editor Body -->
<template #editor="{ data, field }">
<template v-if="field === 'Module'">
<InputText
v-model="data[field].userDefinedName"
class="w-full"
autofocus
/>
</template>
<template v-else-if="field === 'Reminder'">
<!--<InputSwitch
v-model="data.Module.reminder"
class="align-self-center"
/>-->
<Button
:icon="data.Module.reminder ? 'pi pi-bell' : 'pi pi-times'"
:severity="data.Module.reminder ? 'warning' : 'secondary'"
rounded
outlined
class="small-button"
@click = "data.Module.reminder = !data.Module.reminder"
></Button>
</template>
<template v-else>
<div>{{ data[field] }}</div>
</template>
</template>
</Column>
</DataTable>
@@ -75,4 +114,10 @@ async function finalStep() {
</div>
</template>
<style scoped></style>
<style scoped>
.small-button.p-button {
width: 2rem;
height: 2rem;
padding: 0;
}
</style>

View File

@@ -20,6 +20,7 @@ const tableData = computed(() =>
const columns = ref([
{ field: "Course", header: "Course" },
{ field: "Module", header: "Module" },
{ field: "Reminder", header: "Reminder"}
]);
const fetchedModules = async () => {
@@ -61,36 +62,68 @@ async function finalStep() {
table-class="editable-cells-table"
responsive-layout="scroll"
>
<template #header>
<div class="flex align-items-center justify-content-end">
Enable all notifications:
<InputSwitch
class="mx-4"
:model-value="tableData.reduce((acc, curr) => acc && curr.Module.reminder, true)"
@update:model-value="tableData.forEach((module) => module.Module.reminder = $event)"
/>
</div>
</template>
<Column
v-for="col of columns"
:key="col.field"
:field="col.field"
:header="col.header"
:class="col.field === 'Reminder' ? 'text-center' : ''"
>
<!-- Text Body -->
<template #body="{ data, field }">
<div>
{{
field === "Module" ? data[field].userDefinedName : data[field]
}}
</div>
</template>
<template #editor="{ data, field }">
<template v-if="field !== 'Module'">
<div>{{ data[field] }}</div>
<template v-if="field === 'Module'">
{{ data[field].userDefinedName }}
</template>
<template v-else-if="field === 'Reminder'" class="align-content-center">
<Button
:icon="data.Module.reminder ? 'pi pi-bell' : 'pi pi-times'"
:severity="data.Module.reminder ? 'warning' : 'secondary'"
rounded
outlined
class="small-button"
@click = "data.Module.reminder = !data.Module.reminder"
></Button>
</template>
<template v-else>
{{ data[field] }}
</template>
</template>
<!-- Editor Body -->
<template #editor="{ data, field }">
<template v-if="field === 'Module'">
<InputText
v-model="data[field].userDefinedName"
class="w-full"
autofocus
/>
</template>
<template v-else-if="field === 'Reminder'">
<Button
:icon="data.Module.reminder ? 'pi pi-bell' : 'pi pi-times'"
:severity="data.Module.reminder ? 'warning' : 'secondary'"
rounded
outlined
class="small-button"
@click = "data.Module.reminder = !data.Module.reminder"
></Button>
</template>
</template>
</Column>
<Column>
<template #body="{ data }">
<Button
icon="pi pi-trash"
class="small-button"
severity="danger"
outlined
rounded
@@ -109,4 +142,10 @@ async function finalStep() {
</div>
</template>
<style scoped></style>
<style scoped>
.small-button.p-button {
width: 2rem;
height: 2rem;
padding: 0;
}
</style>

View File

@@ -6,6 +6,7 @@ import Button from "primevue/button";
import Dropdown from "primevue/dropdown";
import Menubar from "primevue/menubar";
import InputText from "primevue/inputtext";
import InputSwitch from "primevue/inputswitch";
import Card from "primevue/card";
import DataView from "primevue/dataview";
import Dialog from "primevue/dialog";
@@ -42,6 +43,7 @@ app.component("Menubar", Menubar);
app.component("Dialog", Dialog);
app.component("Dropdown", Dropdown);
app.component("InputText", InputText);
app.component("InputSwitch", InputSwitch);
app.component("Card", Card);
app.component("DataView", DataView);
app.component("ToggleButton", ToggleButton);

View File

@@ -8,6 +8,7 @@ export class Module {
public userDefinedName: string,
public prof: string,
public semester: string,
public reminder: boolean,
public events: Event[] = [],
) {}