Compare commits
13 Commits
86f71bf178
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
1c6023beab
|
|||
|
f559d3a91b
|
|||
|
1e971a0601
|
|||
|
32b31814e2
|
|||
|
1f9543909a
|
|||
|
b4118f7b25
|
|||
|
abddc62f8c
|
|||
|
939633b675
|
|||
|
95766c62a0
|
|||
|
3094ec1501
|
|||
|
ed5e493912
|
|||
|
38b494f066
|
|||
|
448b565835
|
@@ -2,5 +2,10 @@ EMAIL=username@spsejecna.cz
|
||||
PASSWORD=mojesupertajneheslo
|
||||
SHAREPOINT_URL=https://spsejecnacz.sharepoint.com/:x:/s/nastenka/ESy19K245Y9BouR5ksciMvgBu3Pn_9EaT0fpP8R6MrkEmg
|
||||
|
||||
# API for announcing errors and other global stuff for the client
|
||||
# This server is open-source too, tho I don't recommend hosting it yourself,
|
||||
# since it is used for global announcements and if you host it yourself, you won't get the latest news.
|
||||
ANNOUNCEMENT_API=https://announcement.jecnarozvrh.jzitnik.dev
|
||||
|
||||
# For the viewer
|
||||
API_URl=http://localhost:3000
|
||||
|
||||
@@ -9,5 +9,9 @@ services:
|
||||
NODE_ENV: production
|
||||
EMAIL: username@spsejecna.cz
|
||||
PASSWORD: mojesupertajneheslo
|
||||
# API for announcing errors and other global stuff for the client
|
||||
# This server is open-source too, tho I don't recommend hosting it yourself,
|
||||
# since it is used for global announcements and if you host it yourself, you won't get the latest news.
|
||||
ANNOUNCEMENT_API: https://announcement.jecnarozvrh.jzitnik.dev
|
||||
volumes:
|
||||
- ./volume:/usr/src/app/volume
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
const API_BASE_URL = process.env.ANNOUNCEMENT_API || "http://localhost:3000";
|
||||
|
||||
export type Announcement = {
|
||||
id: number;
|
||||
text_content?: string;
|
||||
author: string;
|
||||
classes: string[];
|
||||
flags: Flag[];
|
||||
start_date: string;
|
||||
end_date: string;
|
||||
};
|
||||
|
||||
export enum Flag {
|
||||
SHOW_ALL_ENTRIES
|
||||
}
|
||||
|
||||
export type AnnouncementResponse = {
|
||||
[date: string]: Announcement[];
|
||||
}
|
||||
|
||||
export default async function getAnnouncements(dates: string[]): Promise<AnnouncementResponse> {
|
||||
if (dates.length === 0) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const url = new URL(`/v1/announcements/${dates.join(",")}`, API_BASE_URL).toString();
|
||||
|
||||
try {
|
||||
const response = await fetch(url);
|
||||
const data = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return data;
|
||||
} catch (e) {
|
||||
console.error("Some random ahh error:", e);
|
||||
|
||||
return {};
|
||||
}
|
||||
}
|
||||
+12
-3
@@ -12,10 +12,19 @@
|
||||
* GNU General Public License for more details.
|
||||
*/
|
||||
|
||||
import parseV1V2 from "./parse/v1_v2.js";
|
||||
import ExcelJS from "exceljs"
|
||||
|
||||
//import parseV1V2 from "./parse/v1_v2.js";
|
||||
import parseV3 from "./parse/v3.js";
|
||||
import generateArchivedV1_V2 from "./parse/archived/v1_v2.js";
|
||||
|
||||
export default async function parseThisShit(downloadedFilePath: string) {
|
||||
await parseV1V2(downloadedFilePath);
|
||||
await parseV3(downloadedFilePath);
|
||||
const workbook = new ExcelJS.Workbook();
|
||||
await workbook.xlsx.readFile(downloadedFilePath);
|
||||
|
||||
//await parseV1V2(workbook);
|
||||
await generateArchivedV1_V2();
|
||||
await parseV3(workbook, downloadedFilePath);
|
||||
}
|
||||
|
||||
//parseThisShit("volume/db/current.xlsx")
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
import fs from "fs/promises"
|
||||
|
||||
const CLASSES: string[] = [
|
||||
"A1a", "A1b", "A1c", "C1a", "C1b", "C1c", "A2", "C2a", "C2b", "C2c", "E2", "C3a", "C3b", "C3c", "E3"
|
||||
];
|
||||
|
||||
function formatDate(date: Date): string {
|
||||
const year = date.getFullYear();
|
||||
const month = String(date.getMonth() + 1).padStart(2, "0");
|
||||
const day = String(date.getDate()).padStart(2, "0");
|
||||
return `${year}-${month}-${day}`;
|
||||
}
|
||||
|
||||
const EXAMPLE_SCHEDULE: (string | null)[] = [
|
||||
"Verze 1 API bude ukončena",
|
||||
"Prosím aktualizujte si aplikaci.",
|
||||
"Po ukončení Vám nebude fungovat",
|
||||
"mimořádný rozvrh.",
|
||||
null,
|
||||
"Pokud jste vývojář aplikace,",
|
||||
"podívejte se na dokumentaci na",
|
||||
"jecnarozvrh.jzitnik.dev",
|
||||
null,
|
||||
null
|
||||
];
|
||||
|
||||
function getCurrentWeekMondayToFriday(referenceDate: Date = new Date()): string[] {
|
||||
const date = new Date(referenceDate);
|
||||
const day = date.getDay();
|
||||
const diffToMonday = day === 0 ? -6 : 1 - day;
|
||||
const monday = new Date(date);
|
||||
monday.setDate(date.getDate() + diffToMonday);
|
||||
const week: string[] = [];
|
||||
for (let i = 0; i < 5; i++) {
|
||||
const d = new Date(monday);
|
||||
d.setDate(monday.getDate() + i);
|
||||
week.push(formatDate(d));
|
||||
}
|
||||
return week;
|
||||
}
|
||||
|
||||
interface ScheduleData {
|
||||
schedule: Record<string, (string | null)[]>[];
|
||||
props: { date: string; priprava: boolean }[];
|
||||
status: { lastUpdated: string };
|
||||
}
|
||||
|
||||
export default async function generateArchivedV1_V2() {
|
||||
const dates = getCurrentWeekMondayToFriday();
|
||||
const currentDate = new Date();
|
||||
const lastUpdated = currentDate.getHours().toString().padStart(2, "0") + ":" + currentDate.getMinutes().toString().padStart(2, "0");
|
||||
|
||||
const data: ScheduleData = {
|
||||
schedule: [],
|
||||
props: [],
|
||||
status: {
|
||||
lastUpdated
|
||||
}
|
||||
};
|
||||
|
||||
for (const date of dates) {
|
||||
data.props.push({
|
||||
date,
|
||||
priprava: false
|
||||
});
|
||||
|
||||
const d: Record<string, (string | null)[]> = {};
|
||||
|
||||
for (const cls of CLASSES) {
|
||||
d[cls] = EXAMPLE_SCHEDULE;
|
||||
}
|
||||
|
||||
data.schedule.push(d);
|
||||
}
|
||||
|
||||
await Promise.all([
|
||||
fs.writeFile("volume/db/v1.json", JSON.stringify(data, null, 2)),
|
||||
fs.writeFile("volume/db/v2.json", JSON.stringify(data, null, 2)),
|
||||
]);
|
||||
}
|
||||
@@ -12,7 +12,7 @@
|
||||
* GNU General Public License for more details.
|
||||
*/
|
||||
|
||||
import ExcelJS, { Worksheet } from "exceljs"
|
||||
import { Workbook, Worksheet } from "exceljs"
|
||||
import fs from "fs"
|
||||
import parseAbsence, { AbsenceResult } from "../utils/parseAbsence.js"
|
||||
import parseTeachers from "../utils/parseTeachers.js"
|
||||
@@ -27,9 +27,7 @@ interface ScheduleDay {
|
||||
ABSENCE?: AbsenceResult[];
|
||||
}
|
||||
|
||||
export default async function parseV1V2(downloadedFilePath: string) {
|
||||
const workbook = new ExcelJS.Workbook();
|
||||
await workbook.xlsx.readFile(downloadedFilePath);
|
||||
export default async function parseV1V2(workbook: Workbook) {
|
||||
const teacherMap = await parseTeachers();
|
||||
|
||||
const dateRegex = /^(pondělí|úterý|středa|čtvrtek|pátek|po|út|ut|st|čt|ct|pa|pá)\s+(\d{1,2})\.\s*(\d{1,2})\.\s*(\d{4}|\d{2})/i;
|
||||
|
||||
+45
-18
@@ -15,9 +15,10 @@
|
||||
import fs from "fs";
|
||||
import parseAbsence from "../utils/parseAbsence.js"
|
||||
import parseTeachers from "../utils/parseTeachers.js"
|
||||
import ExcelJS, { Worksheet, Cell, Row } from "exceljs"
|
||||
import ExcelJS, { Worksheet, Cell, Row, Workbook } from "exceljs"
|
||||
import JSZip from "jszip";
|
||||
import { parseStringPromise } from "xml2js";
|
||||
import getAnnouncements, { Flag } from "../api/announcements.js";
|
||||
|
||||
interface ThemeColors {
|
||||
[key: number]: string | null;
|
||||
@@ -42,8 +43,6 @@ async function getThemeColors(filePath: string): Promise<ThemeColors | null> {
|
||||
const data = fs.readFileSync(filePath);
|
||||
const zip = await JSZip.loadAsync(data);
|
||||
|
||||
// list all files for debug
|
||||
|
||||
const themeFile = zip.file("xl/theme/theme1.xml");
|
||||
if (!themeFile) {
|
||||
return null;
|
||||
@@ -118,9 +117,7 @@ function resolveCellColor(cell: Cell, themeColors: ThemeColors | null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
export default async function parseV3(downloadedFilePath: string) {
|
||||
const workbook = new ExcelJS.Workbook();
|
||||
await workbook.xlsx.readFile(downloadedFilePath);
|
||||
export default async function parseV3(workbook: Workbook, downloadedFilePath: string) {
|
||||
const themeColors = await getThemeColors(downloadedFilePath);
|
||||
|
||||
const teacherMap = await parseTeachers();
|
||||
@@ -128,10 +125,20 @@ export default async function parseV3(downloadedFilePath: string) {
|
||||
const upcoming = getUpcomingSheets(workbook);
|
||||
const resolvedDays = groupSheetsByDate(upcoming);
|
||||
|
||||
const sheetDates = resolvedDays.map((d) => d.dateKey);
|
||||
const announcementDates = getWeekDates().concat(sheetDates);
|
||||
if (new Date().getDay() === 0) {
|
||||
announcementDates.push(...getWeekDates(7));
|
||||
}
|
||||
const announcements = await getAnnouncements([...new Set(announcementDates)]);
|
||||
|
||||
const schedule: any = {};
|
||||
|
||||
for (const { dateKey, sheet } of resolvedDays) {
|
||||
const { changes, absence, inWork, takesPlace, reservedRooms } = extractDaySchedule(sheet, teacherMap, themeColors);
|
||||
const ann = announcements[dateKey];
|
||||
const allFlags = ann.map(a => a.flags).flat();
|
||||
|
||||
const { changes, absence, inWork, takesPlace, reservedRooms } = extractDaySchedule(sheet, teacherMap, themeColors, allFlags);
|
||||
|
||||
schedule[dateKey] = {
|
||||
info: { inWork },
|
||||
@@ -144,6 +151,7 @@ export default async function parseV3(downloadedFilePath: string) {
|
||||
|
||||
const data = {
|
||||
status: { lastUpdated: formatNowTime() },
|
||||
announcements,
|
||||
schedule,
|
||||
};
|
||||
|
||||
@@ -157,7 +165,7 @@ export default async function parseV3(downloadedFilePath: string) {
|
||||
//
|
||||
|
||||
function getUpcomingSheets(workbook: ExcelJS.Workbook): ResolvedDay[] {
|
||||
const dateRegex = /^(pondělí|úterý|středa|čtvrtek|pátek|po|út|ut|st|čt|ct|pa|pá)\s+(\d{1,2})\.\s*(\d{1,2})\.\s*(\d{4}|\d{2})/i;
|
||||
const dateRegex = /^(pondělí|úterý|středa|čtvrtek|pátek|po|út|ut|st|čt|ct|pa|pá)\s+(\d{1,2})\.+\s*(\d{1,2})\.+\s*(\d{4}|\d{2})/i;
|
||||
|
||||
const today = new Date();
|
||||
const todayMidnight = new Date(today.getFullYear(), today.getMonth(), today.getDate());
|
||||
@@ -206,9 +214,9 @@ function groupSheetsByDate(items: ResolvedDay[]) {
|
||||
// ────────────────────────────────────────────────────────────
|
||||
//
|
||||
|
||||
function extractDaySchedule(sheet: Worksheet, teacherMap: Record<string, string>, themeColors: ThemeColors | null) {
|
||||
function extractDaySchedule(sheet: Worksheet, teacherMap: Record<string, string>, themeColors: ThemeColors | null, flags: Flag[]) {
|
||||
return {
|
||||
changes: extractClassChanges(sheet, themeColors),
|
||||
changes: extractClassChanges(sheet, themeColors, flags),
|
||||
absence: extractAbsence(sheet, teacherMap),
|
||||
inWork: isPripravaSheet(sheet.name.toLowerCase()),
|
||||
takesPlace: extractTakesPlace(sheet),
|
||||
@@ -230,7 +238,9 @@ function isPripravaSheet(name: string) {
|
||||
// ────────────────────────────────────────────────────────────
|
||||
//
|
||||
|
||||
function extractClassChanges(sheet: Worksheet, themeColors: ThemeColors | null) {
|
||||
function extractClassChanges(sheet: Worksheet, themeColors: ThemeColors | null, flags: Flag[]) {
|
||||
console.log(flags)
|
||||
const ignoreColors = flags.includes(Flag.SHOW_ALL_ENTRIES)
|
||||
const classRegex = /[AEC][0-4][a-c]?\s*\/.*/s;
|
||||
const prefixRegex = /[AEC][0-4][a-c]?/;
|
||||
|
||||
@@ -252,20 +262,20 @@ function extractClassChanges(sheet: Worksheet, themeColors: ThemeColors | null)
|
||||
|
||||
classCells.forEach((address, index) => {
|
||||
const row = sheet.getRow(Number(sheet.getCell(address).row));
|
||||
changes[classes[index]] = buildLessonArray(row, address, themeColors);
|
||||
changes[classes[index]] = buildLessonArray(row, address, themeColors, ignoreColors);
|
||||
});
|
||||
|
||||
return changes;
|
||||
}
|
||||
|
||||
function buildLessonArray(row: Row, ignoreAddress: string, themeColors: ThemeColors | null) {
|
||||
function buildLessonArray(row: Row, ignoreAddress: string, themeColors: ThemeColors | null, ignoreColors: boolean) {
|
||||
const lessons: (Lesson | null)[] = [];
|
||||
|
||||
row.eachCell((cell) => {
|
||||
if (cell.address === ignoreAddress) return;
|
||||
|
||||
const colIndex = letterToNumber(cell.address.replace(/[0-9]/g, ""));
|
||||
lessons[colIndex] = parseLessonCell(cell, themeColors);
|
||||
lessons[colIndex] = parseLessonCell(cell, themeColors, ignoreColors);
|
||||
});
|
||||
|
||||
const normalized = Array.from(lessons, (x) => (x === undefined ? null : x));
|
||||
@@ -274,13 +284,13 @@ function buildLessonArray(row: Row, ignoreAddress: string, themeColors: ThemeCol
|
||||
return normalized.slice(1, 11);
|
||||
}
|
||||
|
||||
function parseLessonCell(cell: Cell, themeColors: ThemeColors | null): Lesson | null {
|
||||
function parseLessonCell(cell: Cell, themeColors: ThemeColors | null, ignoreColors: boolean): Lesson | null {
|
||||
try {
|
||||
const text = (cell.text || "").trim();
|
||||
const cleanupRegex = /^úklid\s+(?:\d+\s+)?[A-Za-z]{2}$/;
|
||||
|
||||
// @ts-ignore
|
||||
if (!text || cleanupRegex.test(text) || !cell.fill?.fgColor) return null;
|
||||
if (!text || cleanupRegex.test(text) || (!cell.fill?.fgColor && !ignoreColors)) return null;
|
||||
|
||||
const backgroundColor = resolveCellColor(cell, themeColors);
|
||||
const foregroundColor = !backgroundColor ? undefined : (
|
||||
@@ -419,6 +429,25 @@ function letterToNumber(letter: string) {
|
||||
return letter.toLowerCase().charCodeAt(0) - 97;
|
||||
}
|
||||
|
||||
function getWeekDates(offset: number = 0): string[] {
|
||||
const today = new Date();
|
||||
const day = today.getDay();
|
||||
const monday = new Date(today);
|
||||
monday.setDate(today.getDate() + (day === 0 ? -6 : 1 - day) + offset);
|
||||
|
||||
const dates: string[] = [];
|
||||
for (let i = 0; i < 5; i++) {
|
||||
const d = new Date(monday);
|
||||
d.setDate(monday.getDate() + i);
|
||||
dates.push(formatDateKey(d));
|
||||
}
|
||||
return dates;
|
||||
}
|
||||
|
||||
function formatDateKey(date: Date): string {
|
||||
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}-${String(date.getDate()).padStart(2, "0")}`;
|
||||
}
|
||||
|
||||
function formatNowTime() {
|
||||
const now = new Date();
|
||||
return (
|
||||
@@ -427,5 +456,3 @@ function formatNowTime() {
|
||||
now.getMinutes().toString().padStart(2, "0")
|
||||
);
|
||||
}
|
||||
|
||||
//parseV3("volume/db/current.xlsx")
|
||||
|
||||
@@ -8,7 +8,7 @@ TocOpen: true
|
||||
|
||||
Ječná Rozvrh API má svoji Rust knihovnu pro komunikaci s API. Obsahuje mappings pro Kotlin. Pro další jazyky budou mappingy v budoucnu.
|
||||
|
||||
[Zdrojový kód knihovny](https://gitea.local.jzitnik.dev/jzitnik/jecna-supl-client)
|
||||
[Zdrojový kód knihovny](https://gitea.jzitnik.dev/jzitnik/jecna-supl-client)
|
||||
|
||||
## Usage
|
||||
|
||||
|
||||
@@ -6,6 +6,8 @@ tags: ["api", "docs"]
|
||||
|
||||
Vítejte v dokumentaci pro API systému Ječná Rozvrh. Toto API poskytuje programový přístup k rozvrhům, suplování a dalším informacím.
|
||||
|
||||
OpenAPI spec a SwaggerUI je v přípravě!
|
||||
|
||||
## Oficiální knihovna
|
||||
|
||||
[Oficiální knihovna pro komunikaci s Ječná Rozvrh API](../lib)
|
||||
@@ -36,16 +38,14 @@ API je verzované, aby byla zajištěna zpětná kompatibilita. Zde je seznam do
|
||||
|
||||
Toto je aktuální a doporučená verze API. Obsahuje velké změny oproti V2 a obsahuje nové data.
|
||||
|
||||
- ### [Verze 2 (v2)](../v2)
|
||||
**Status:** Stabilní
|
||||
- ### [~Verze 2 (v2)~](../v2)
|
||||
**Status:** Archivováno (API již není v provozu)
|
||||
**Endpoint:** `/versioned/v2`
|
||||
|
||||
- ### [~Verze 1 (v1)~](../v1)
|
||||
**Status:** Deprecated
|
||||
**Status:** Archivováno (API již není v provozu)
|
||||
**Endpoint:** `/versioned/v1`
|
||||
|
||||
Verze 1 bude v budoucnu odstaněna. Migrujte na novější verze
|
||||
|
||||
---
|
||||
|
||||
## Nezařazené Endpointy
|
||||
|
||||
@@ -5,8 +5,8 @@ tags: ["api", "docs", "v1"]
|
||||
hiddenInHomelist: true
|
||||
---
|
||||
|
||||
{{< admonition type="warning" title="Deprecated" >}}
|
||||
Tato verze je **deprecated**. Prosím nepoužívejte ji, bude brzy odstraněna.
|
||||
{{< admonition type="warning" title="Archoviváno" >}}
|
||||
Tato verze je **archovována**. API již není v provozu.
|
||||
{{< /admonition >}}
|
||||
|
||||
Tato stránka detailně popisuje **Verzi 1 (v1)** API Ječná Rozvrh.
|
||||
|
||||
@@ -5,6 +5,10 @@ tags: ["api", "docs", "v2"]
|
||||
hiddenInHomelist: true
|
||||
---
|
||||
|
||||
{{< admonition type="warning" title="Archoviváno" >}}
|
||||
Tato verze je **archovována**. API již není v provozu.
|
||||
{{< /admonition >}}
|
||||
|
||||
Tato stránka detailně popisuje **Verzi 2 (v2)** API Ječná Rozvrh.
|
||||
|
||||
## Endpoint: `GET /versioned/v2`
|
||||
|
||||
@@ -5,8 +5,6 @@ tags: ["code", "contribution"]
|
||||
hiddenInHomelist: true
|
||||
---
|
||||
|
||||
# Contributing
|
||||
|
||||
Děkuji za váš zájem přispět do tohoto projektu.
|
||||
Příspěvky jsou vítány, ale **přijímám je pouze e-mailem ve formě `.patch` souborů**.
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ Použití Dockeru je nejjednodušší způsob, jak projekt spustit, protože aut
|
||||
|
||||
1. **Klonování repozitáře**:
|
||||
```bash
|
||||
git clone https://gitea.local.jzitnik.dev/jzitnik/jecnarozvrh.git
|
||||
git clone https://gitea.jzitnik.dev/jzitnik/jecnarozvrh.git
|
||||
cd jecnarozvrh
|
||||
```
|
||||
|
||||
|
||||
Reference in New Issue
Block a user