mirror of
https://gitlab.dit.htwk-leipzig.de/htwk-software/htwkalender-pwa.git
synced 2025-08-07 04:09:17 +02:00
test:#13 added tests and added licence
This commit is contained in:
@@ -1,3 +1,21 @@
|
|||||||
|
<!--
|
||||||
|
Calendar implementation for the HTWK Leipzig timetable. Evaluation and display of the individual dates in iCal format.
|
||||||
|
Copyright (C) 2024 HTWKalender support@htwkalender.de
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
-->
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
|
||||||
import FullCalendar from "@fullcalendar/vue3";
|
import FullCalendar from "@fullcalendar/vue3";
|
||||||
@@ -9,7 +27,7 @@ import interactionPlugin from "@fullcalendar/interaction";
|
|||||||
import timeGridPlugin from "@fullcalendar/timegrid";
|
import timeGridPlugin from "@fullcalendar/timegrid";
|
||||||
import iCalenderPlugin from "@fullcalendar/icalendar";
|
import iCalenderPlugin from "@fullcalendar/icalendar";
|
||||||
import router from "@/router";
|
import router from "@/router";
|
||||||
import { formatYearMonthDay } from "@/helpers/dates.ts";
|
import { formatYearMonthDay, removeTZ } from "@/helpers/dates.ts";
|
||||||
import { useI18n } from "vue-i18n";
|
import { useI18n } from "vue-i18n";
|
||||||
import { useQuery } from "@tanstack/vue-query";
|
import { useQuery } from "@tanstack/vue-query";
|
||||||
import tokenStore from "@/store/tokenStore.ts";
|
import tokenStore from "@/store/tokenStore.ts";
|
||||||
@@ -29,42 +47,26 @@ const op = ref();
|
|||||||
const clickedEvent = ref();
|
const clickedEvent = ref();
|
||||||
|
|
||||||
const toggle = (info: EventClickArg) => {
|
const toggle = (info: EventClickArg) => {
|
||||||
|
const start = !info.event.start ? "" : removeTZ(info.event.start);
|
||||||
|
const end = !info.event.end ? "" : removeTZ(info.event.end);
|
||||||
|
|
||||||
if (op.value.visible) {
|
if (op.value.visible) {
|
||||||
clickedEvent.value = null;
|
clickedEvent.value = null;
|
||||||
op.value.hide();
|
op.value.hide();
|
||||||
return;
|
return;
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
|
clickedEvent.value = {
|
||||||
|
title: info.event._def.title,
|
||||||
|
start: start,
|
||||||
|
end: end,
|
||||||
|
notes: info.event._def.extendedProps.notes,
|
||||||
|
allDay: info.event._def.allDay,
|
||||||
|
location: info.event._def.extendedProps.location,
|
||||||
|
};
|
||||||
|
op.value.show(info.jsEvent);
|
||||||
|
op.value.target = info.el;
|
||||||
|
|
||||||
const start = info.event.start
|
|
||||||
const end = info.event.end
|
|
||||||
|
|
||||||
if (!start || !end) {
|
|
||||||
clickedEvent.value = {
|
|
||||||
title: info.event._def.title,
|
|
||||||
start: "",
|
|
||||||
end: "",
|
|
||||||
notes: info.event._def.extendedProps.notes,
|
|
||||||
allDay: info.event._def.allDay,
|
|
||||||
location: info.event._def.extendedProps.location,
|
|
||||||
};
|
|
||||||
op.value.show(info.jsEvent);
|
|
||||||
op.value.target = info.el;
|
|
||||||
} else {
|
|
||||||
const timeZoneOffsetStart = start.getTimezoneOffset() * 60000;
|
|
||||||
const timeZoneOffsetEnd = end.getTimezoneOffset() * 60000;
|
|
||||||
|
|
||||||
clickedEvent.value = {
|
|
||||||
title: info.event._def.title,
|
|
||||||
start: new Date(start.getTime() + timeZoneOffsetStart),
|
|
||||||
end: new Date(end.getTime() + timeZoneOffsetEnd),
|
|
||||||
notes: info.event._def.extendedProps.notes,
|
|
||||||
allDay: info.event._def.allDay,
|
|
||||||
location: info.event._def.extendedProps.location,
|
|
||||||
};
|
|
||||||
op.value.show(info.jsEvent);
|
|
||||||
op.value.target = info.el;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@@ -22,3 +22,8 @@
|
|||||||
export function formatYearMonthDay(date: Date): string {
|
export function formatYearMonthDay(date: Date): string {
|
||||||
return date.toISOString().split("T")[0].replace(/-/g, "");
|
return date.toISOString().split("T")[0].replace(/-/g, "");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export function removeTZ(date: Date): Date {
|
||||||
|
return new Date(date.getTime() + date.getTimezoneOffset() * 60000)
|
||||||
|
}
|
141
frontend/src/helpers/ical.test.ts
Normal file
141
frontend/src/helpers/ical.test.ts
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
//Calendar implementation for the HTWK Leipzig timetable. Evaluation and display of the individual dates in iCal format.
|
||||||
|
//Copyright (C) 2024 HTWKalender support@htwkalender.de
|
||||||
|
|
||||||
|
//This program is free software: you can redistribute it and/or modify
|
||||||
|
//it under the terms of the GNU Affero General Public License as published by
|
||||||
|
//the Free Software Foundation, either version 3 of the License, or
|
||||||
|
//(at your option) any later version.
|
||||||
|
|
||||||
|
//This program is distributed in the hope that it will be useful,
|
||||||
|
//but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
//MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
//GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
//You should have received a copy of the GNU Affero General Public License
|
||||||
|
//along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
import { expect, test } from "vitest";
|
||||||
|
import { exportedForTesting } from "@/helpers/ical.ts";
|
||||||
|
import { CalendarComponent } from "ical";
|
||||||
|
|
||||||
|
|
||||||
|
// colorizeEvents has only the function to colorize the events that are passed to it
|
||||||
|
test("colorizeEventsSameSummary", () => {
|
||||||
|
|
||||||
|
const events: CalendarComponent[] =
|
||||||
|
[
|
||||||
|
{
|
||||||
|
type: "VEVENT",
|
||||||
|
summary: "Operations Research",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "VEVENT",
|
||||||
|
summary: "Operations Research",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
expect(exportedForTesting.colorizeEvents(
|
||||||
|
events
|
||||||
|
)
|
||||||
|
).toEqual([{ summary: "Operations Research", color: "var(--htwk-rot-200)" },{ summary: "Operations Research", color: "var(--htwk-rot-200)" }]);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
test("colorizeEventsDifferentSummary", () => {
|
||||||
|
|
||||||
|
const events: CalendarComponent[] =
|
||||||
|
[
|
||||||
|
{
|
||||||
|
type: "VEVENT",
|
||||||
|
summary: "Algorithmische Mathematik",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "VEVENT",
|
||||||
|
summary: "Funktionale Programmierung",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
expect(exportedForTesting.colorizeEvents(
|
||||||
|
events
|
||||||
|
)
|
||||||
|
).toEqual([{ summary: "Algorithmische Mathematik", color: "var(--htwk-rot-200)" },{ summary: "Funktionale Programmierung", color: "var(--htwk-gruen-300)" }]);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
test("filterEventsDistinct", () => {
|
||||||
|
const events: CalendarComponent[] =
|
||||||
|
[
|
||||||
|
{
|
||||||
|
type: "VEVENT",
|
||||||
|
summary: "Operations Research",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "VEVENT",
|
||||||
|
summary: "Operations Research",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
expect(exportedForTesting.filterEventsDistinct(
|
||||||
|
events
|
||||||
|
)
|
||||||
|
).toEqual([{ type: "VEVENT", summary: "Operations Research" }]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("filterEventsDistinctDifferentSummary", () => {
|
||||||
|
const events: CalendarComponent[] =
|
||||||
|
[
|
||||||
|
{
|
||||||
|
type: "VEVENT",
|
||||||
|
summary: "Algorithmische Mathematik",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "VEVENT",
|
||||||
|
summary: "Funktionale Programmierung",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
expect(exportedForTesting.filterEventsDistinct(
|
||||||
|
events
|
||||||
|
)
|
||||||
|
).toEqual(events);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
test("extractedColorizedEvents", () => {
|
||||||
|
const events: CalendarComponent[] =
|
||||||
|
[
|
||||||
|
{
|
||||||
|
type: "VEVENT",
|
||||||
|
summary: "Operations Research",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "VEVENT",
|
||||||
|
summary: "Operations Research",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
expect(exportedForTesting.extractedColorizedEvents(
|
||||||
|
events
|
||||||
|
)
|
||||||
|
).toEqual([{ summary: "Operations Research", color: "var(--htwk-rot-200)" }]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("extractedColorizedEventsDifferentSummary", () => {
|
||||||
|
const events: CalendarComponent[] =
|
||||||
|
[
|
||||||
|
{
|
||||||
|
type: "VEVENT",
|
||||||
|
summary: "Algorithmische Mathematik",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "VEVENT",
|
||||||
|
summary: "Funktionale Programmierung",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
expect(exportedForTesting.extractedColorizedEvents(
|
||||||
|
events
|
||||||
|
)
|
||||||
|
).toEqual([{ summary: "Algorithmische Mathematik", color: "var(--htwk-rot-200)" },{ summary: "Funktionale Programmierung", color: "var(--htwk-gruen-300)" }]);
|
||||||
|
});
|
@@ -1,3 +1,19 @@
|
|||||||
|
//Calendar implementation for the HTWK Leipzig timetable. Evaluation and display of the individual dates in iCal format.
|
||||||
|
//Copyright (C) 2024 HTWKalender support@htwkalender.de
|
||||||
|
|
||||||
|
//This program is free software: you can redistribute it and/or modify
|
||||||
|
//it under the terms of the GNU Affero General Public License as published by
|
||||||
|
//the Free Software Foundation, either version 3 of the License, or
|
||||||
|
//(at your option) any later version.
|
||||||
|
|
||||||
|
//This program is distributed in the hope that it will be useful,
|
||||||
|
//but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
//MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
//GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
//You should have received a copy of the GNU Affero General Public License
|
||||||
|
//along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
import ICAL from "ical.js";
|
import ICAL from "ical.js";
|
||||||
import { CalendarComponent } from "ical";
|
import { CalendarComponent } from "ical";
|
||||||
|
|
||||||
@@ -7,7 +23,7 @@ import { CalendarComponent } from "ical";
|
|||||||
* @param color Color code for the event
|
* @param color Color code for the event
|
||||||
*/
|
*/
|
||||||
export interface ColorDistinctionEvent {
|
export interface ColorDistinctionEvent {
|
||||||
title: string;
|
summary: string;
|
||||||
color: string;
|
color: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -23,7 +39,7 @@ export function parseICalData(icalData: string | undefined): CalendarComponent[]
|
|||||||
const jCalData = ICAL.parse(icalData);
|
const jCalData = ICAL.parse(icalData);
|
||||||
const comp = new ICAL.Component(jCalData);
|
const comp = new ICAL.Component(jCalData);
|
||||||
const vEvents = comp.getAllSubcomponents('vevent');
|
const vEvents = comp.getAllSubcomponents('vevent');
|
||||||
const colorDistinctionEvents = extracted(vEvents);
|
const colorDistinctionEvents = extractedColorizedEvents(vEvents);
|
||||||
|
|
||||||
return vEvents.map((vevent: CalendarComponent) => {
|
return vEvents.map((vevent: CalendarComponent) => {
|
||||||
const event = new ICAL.Event(vevent);
|
const event = new ICAL.Event(vevent);
|
||||||
@@ -34,7 +50,7 @@ export function parseICalData(icalData: string | undefined): CalendarComponent[]
|
|||||||
end: event.endDate.toJSDate(),
|
end: event.endDate.toJSDate(),
|
||||||
notes: event.description,
|
notes: event.description,
|
||||||
allDay: event.startDate.isDate,
|
allDay: event.startDate.isDate,
|
||||||
color: colorDistinctionEvents.find((e: ColorDistinctionEvent) => e.title === event.summary)?.color,
|
color: colorDistinctionEvents.find((e: ColorDistinctionEvent) => e.summary === event.summary)?.color,
|
||||||
id: event.uid,
|
id: event.uid,
|
||||||
location: event.location,
|
location: event.location,
|
||||||
};
|
};
|
||||||
@@ -47,21 +63,33 @@ export function parseICalData(icalData: string | undefined): CalendarComponent[]
|
|||||||
* @param vEvents Array of calendar components
|
* @param vEvents Array of calendar components
|
||||||
* @returns Array of objects with event name and color
|
* @returns Array of objects with event name and color
|
||||||
*/
|
*/
|
||||||
function extracted(vEvents: CalendarComponent[]): ColorDistinctionEvent[] {
|
function extractedColorizedEvents(vEvents: CalendarComponent[]): ColorDistinctionEvent[] {
|
||||||
// filter vevents to get all unique event names
|
return colorizeEvents(filterEventsDistinct(vEvents));
|
||||||
const distinctEvents = vEvents.filter((vevent: CalendarComponent, index: number, self: CalendarComponent[]) => {
|
}
|
||||||
const event = new ICAL.Event(vevent);
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filters out duplicate events
|
||||||
|
* @param vEvents Array of calendar components
|
||||||
|
* @returns Array of calendar components without duplicates
|
||||||
|
*/
|
||||||
|
function filterEventsDistinct(vEvents: CalendarComponent[]): CalendarComponent[] {
|
||||||
|
return vEvents.filter((vevent: CalendarComponent, index: number, self: CalendarComponent[]) => {
|
||||||
return self.findIndex((v) => {
|
return self.findIndex((v) => {
|
||||||
const e = new ICAL.Event(v);
|
return v.summary === vevent.summary;
|
||||||
return e.summary === event.summary;
|
|
||||||
}) === index;
|
}) === index;
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
//create dynamic color distinctionEvents map that will be used to color code events
|
|
||||||
return distinctEvents.map((vevent: CalendarComponent) => {
|
/**
|
||||||
const event = new ICAL.Event(vevent);
|
* Assigns a color to each event
|
||||||
// map event name to a color for color coding css
|
* @param vEvents Array of calendar components
|
||||||
// like var(--htwk-rot-700)
|
* @returns Array of objects with event name and color
|
||||||
|
*/
|
||||||
|
function colorizeEvents(vEvents: CalendarComponent[]): ColorDistinctionEvent[] {
|
||||||
|
|
||||||
|
return vEvents.map((vevent: CalendarComponent) => {
|
||||||
const colors: string[] = [
|
const colors: string[] = [
|
||||||
"var(--htwk-rot-200)",
|
"var(--htwk-rot-200)",
|
||||||
"var(--htwk-gruen-300)",
|
"var(--htwk-gruen-300)",
|
||||||
@@ -76,14 +104,19 @@ function extracted(vEvents: CalendarComponent[]): ColorDistinctionEvent[] {
|
|||||||
"var(--htwk-blau-200)"
|
"var(--htwk-blau-200)"
|
||||||
];
|
];
|
||||||
|
|
||||||
// give each event a color based on its name ascending
|
const randomColor = colors[vEvents.findIndex((e: CalendarComponent) => {
|
||||||
const randomColor = colors[distinctEvents.findIndex((e: CalendarComponent) => {
|
return e.summary === vevent.summary?? "";
|
||||||
const ev = new ICAL.Event(e);
|
|
||||||
return ev.summary === event.summary;
|
|
||||||
}) % colors.length];
|
}) % colors.length];
|
||||||
|
|
||||||
return {
|
return {
|
||||||
title: event.summary,
|
summary: vevent.summary?? "",
|
||||||
color: randomColor
|
color: randomColor
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const exportedForTesting = {
|
||||||
|
extractedColorizedEvents,
|
||||||
|
filterEventsDistinct,
|
||||||
|
colorizeEvents
|
||||||
|
};
|
@@ -1,3 +1,19 @@
|
|||||||
|
//Calendar implementation for the HTWK Leipzig timetable. Evaluation and display of the individual dates in iCal format.
|
||||||
|
//Copyright (C) 2024 HTWKalender support@htwkalender.de
|
||||||
|
|
||||||
|
//This program is free software: you can redistribute it and/or modify
|
||||||
|
//it under the terms of the GNU Affero General Public License as published by
|
||||||
|
//the Free Software Foundation, either version 3 of the License, or
|
||||||
|
//(at your option) any later version.
|
||||||
|
|
||||||
|
//This program is distributed in the hope that it will be useful,
|
||||||
|
//but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
//MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
//GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
//You should have received a copy of the GNU Affero General Public License
|
||||||
|
//along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
const tokenRegex = /^[a-z0-9]{15}$/;
|
const tokenRegex = /^[a-z0-9]{15}$/;
|
||||||
const tokenUriRegex = /[?&]token=([a-z0-9]{15})(?:&|$)/;
|
const tokenUriRegex = /[?&]token=([a-z0-9]{15})(?:&|$)/;
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user