feat: 2026.1.0-alpha.16 – dashboard banner refactor, period auto-activation, text & docs
Dashboard (dashboard/src/dashboard.tsx, settings.tsx, apiAcademicPeriods.ts): - Refactor loadHolidayStatus to useCallback with stable empty-deps reference; removes location.pathname dependency that caused overlapping API calls at mount and left the banner unresolved via request-sequence cancellation - Add key prop derived from severity:text to Syncfusion MessageComponent to force remount on state change, fixing stale banner that ignored React prop/children updates - Correct German transliterated text to proper Umlauts throughout visible UI strings (fuer -> für, oe -> ö, ae -> ä etc. across dashboard and settings views) Backend (server/init_academic_periods.py): - Refactor to idempotent two-phase flow: seed default periods only when table is empty; on every run activate exactly the non-archived period covering date.today() - Enforces single-active invariant by deactivating all periods before promoting match - Emits explicit warning when no period covers current date instead of doing nothing Deployment (docker-compose.prod.yml): - Add init_academic_periods.py to server startup chain after migrations and defaults; eliminates manual post-deploy step to set an active academic period Release docs: - program-info.json: bump to 2026.1.0-alpha.16; fix JSON parse error caused by typographic curly quotes in the new changelog entry - TECH-CHANGELOG.md: detailed alpha.16 section with root-cause motivation for both dashboard refactoring decisions (unstable callback ref + Syncfusion stale render) - DEV-CHANGELOG.md: document dashboard refactor, Syncfusion key fix, Umlaut changes, and program-info JSON regression and fix - README.md: add Latest Release Highlights section for alpha.16 - .github/copilot-instructions.md: sync file map, prod bootstrap note, backend and frontend pattern additions for academic period init and Syncfusion remount pattern
This commit is contained in:
5
.github/copilot-instructions.md
vendored
5
.github/copilot-instructions.md
vendored
@@ -16,6 +16,8 @@ It is not a changelog and not a full architecture handbook.
|
||||
- `scheduler/scheduler.py` - scheduler loop, MQTT event publishing, TV power intent publishing
|
||||
- `scheduler/db_utils.py` - event formatting and power-intent helper logic
|
||||
- `listener/listener.py` - discovery/heartbeat/log/screenshot MQTT consumption
|
||||
- `server/init_academic_periods.py` - idempotent academic-period seeding + auto-activation for current date
|
||||
- `server/initialize_database.py` - migration + bootstrap orchestration for local/manual setup
|
||||
- `server/routes/events.py` - event CRUD, recurrence handling, UTC normalization
|
||||
- `server/routes/eventmedia.py` - file manager, media upload/stream endpoints
|
||||
- `server/routes/groups.py` - group lifecycle, alive status, order persistence
|
||||
@@ -32,6 +34,7 @@ It is not a changelog and not a full architecture handbook.
|
||||
- Listener: MQTT consumer that updates server-side state
|
||||
- Scheduler: publishes active events and group-level TV power intents
|
||||
- Nginx: routes `/api/*` and `/screenshots/*` to API, dashboard otherwise
|
||||
- Prod bootstrap: `docker-compose.prod.yml` server command runs migrations, defaults init, and academic-period init before Gunicorn start
|
||||
|
||||
## Non-negotiable conventions
|
||||
- Datetime:
|
||||
@@ -66,12 +69,14 @@ TV power intent Phase 1 rules:
|
||||
- Keep enum/datetime serialization JSON-safe.
|
||||
- Maintain UTC-safe comparisons in scheduler and routes.
|
||||
- Keep recurrence handling backend-driven and consistent with exceptions.
|
||||
- Academic periods bootstrap is idempotent and should auto-activate period covering `date.today()` when available.
|
||||
|
||||
## Frontend patterns
|
||||
- Use Syncfusion-based patterns already present in dashboard.
|
||||
- Keep API requests relative (`/api/...`) to use Vite proxy in dev.
|
||||
- Respect `FRONTEND_DESIGN_RULES.md` for component and styling conventions.
|
||||
- Keep role-gated UI behavior aligned with backend authorization.
|
||||
- Holiday status banner in dashboard should render from computed state and avoid stale message reuse in 3rd-party UI components.
|
||||
|
||||
## Environment variables (high-value)
|
||||
- Scheduler: `POLL_INTERVAL_SECONDS`, `REFRESH_SECONDS`
|
||||
|
||||
@@ -5,6 +5,10 @@ This changelog tracks all changes made in the development workspace, including i
|
||||
---
|
||||
|
||||
## Unreleased (development workspace)
|
||||
- Programminfo GUI regression/fix: `dashboard/public/program-info.json` could not be loaded in Programminfo menu due to invalid JSON in the new alpha.16 changelog line (malformed quote in a text entry). Fixed JSON entry and verified file parses correctly again.
|
||||
- Dashboard holiday banner fix: `dashboard/src/dashboard.tsx` — `loadHolidayStatus` now uses a stable `useCallback` with empty deps, preventing repeated re-creation on render. `useEffect` depends only on the stable callback reference.
|
||||
- Dashboard Syncfusion stale-render fix: `MessageComponent` in the holiday banner now receives `key={`${severity}:${text}`}` to force remount when severity or text changes; without this Syncfusion cached stale DOM and the banner did not update reactively.
|
||||
- Dashboard German text: Replaced transliterated forms (ae/oe/ue) with correct Umlauts throughout visible dashboard UI strings — `Präsentation`, `für`, `prüfen`, `Ferienüberschneidungen`, `verfügbar`, `Vorfälle`, `Ausfälle`.
|
||||
- TV power intent (Phase 1): Scheduler publishes retained QoS1 group-level intents to `infoscreen/groups/{group_id}/power/intent` with transition+heartbeat semantics, startup/reconnect republish, and poll-based expiry (`max(3 × poll_interval_sec, 90s)`).
|
||||
- TV power validation: Added unit/integration/canary coverage in `scheduler/test_power_intent_utils.py`, `scheduler/test_power_intent_scheduler.py`, and `test_power_intent_canary.py`.
|
||||
- Monitoring system completion: End-to-end monitoring pipeline is active (MQTT logs/health → listener persistence → monitoring APIs → superadmin dashboard).
|
||||
|
||||
@@ -15,6 +15,13 @@ Core stack:
|
||||
- Messaging: MQTT (Mosquitto)
|
||||
- Background jobs: Redis + RQ + Gotenberg
|
||||
|
||||
## Latest Release Highlights (2026.1.0-alpha.16)
|
||||
|
||||
- Dashboard holiday status banner now updates reliably after hard refresh and after switching between settings and dashboard.
|
||||
- Production startup now auto-initializes and auto-activates the academic period for the current date.
|
||||
- Dashboard German UI wording was polished with proper Umlauts.
|
||||
- User-facing changelog source: [dashboard/public/program-info.json](dashboard/public/program-info.json)
|
||||
|
||||
## Architecture (Short)
|
||||
|
||||
- Dashboard talks only to API (`/api/...` via Vite proxy in dev).
|
||||
|
||||
@@ -5,6 +5,29 @@
|
||||
|
||||
This changelog documents technical and developer-relevant changes included in public releases. For development workspace changes, see DEV-CHANGELOG.md. Not all changes here are reflected in the user-facing changelog (`program-info.json`), and not all UI/feature changes are repeated here. Some changes (e.g., backend refactoring, API adjustments, infrastructure, developer tooling, or internal logic) may only appear in TECH-CHANGELOG.md. For UI/feature changes, see `dashboard/public/program-info.json`.
|
||||
|
||||
## 2026.1.0-alpha.16 (2026-04-02)
|
||||
- 🐛 **Dashboard holiday banner refactoring and state fix (`dashboard/src/dashboard.tsx`)**:
|
||||
- **Motivation — unstable fetch function:** `loadHolidayStatus` had `location.pathname` in its `useCallback` dependency array, causing a new function reference to be created on every navigation event. The `useEffect` depending on that reference then re-fired, producing overlapping API calls at mount that cancelled each other via the request-sequence guard, leaving the banner unresolved.
|
||||
- **Refactoring:** Removed `location.pathname` from `useCallback` deps (it was unused inside the function body). The callback now has an empty dependency array, making its reference stable across the component lifetime. The `useEffect` is keyed only to the stable callback reference — no spurious re-fires.
|
||||
- **Motivation — Syncfusion stale render:** Syncfusion's `MessageComponent` caches its rendered DOM internally and does not reactively update when React passes new children or props. Even after React state changed, the component displayed whatever text was rendered on first mount.
|
||||
- **Fix:** Added a `key` prop derived from `${severity}:${text}` to `MessageComponent`. React unmounts and remounts the component whenever the key changes, bypassing Syncfusion's internal caching and ensuring the correct message is always visible.
|
||||
- **Result:** Active-period name and holiday overlap details now render correctly on hard refresh, initial load, and route transitions without additional API calls.
|
||||
- 🗓️ **Academic period bootstrap hardening (`server/init_academic_periods.py`)**:
|
||||
- Refactored initialization into idempotent flow:
|
||||
- seed default periods only when table is empty,
|
||||
- on every run, activate exactly the non-archived period covering `date.today()`.
|
||||
- Enforces single-active behavior by deactivating all previously active periods before setting the period for today.
|
||||
- Emits explicit warning if no period covers current date (all remain inactive), improving operational diagnostics.
|
||||
- 🚀 **Production startup alignment (`docker-compose.prod.yml`)**:
|
||||
- Server startup command now runs `python /app/server/init_academic_periods.py` after migrations and default settings bootstrap.
|
||||
- Removes manual post-deploy step to set an active academic period.
|
||||
- 🌐 **Dashboard UX/text refinement (`dashboard/src/dashboard.tsx`)**:
|
||||
- Converted user-facing transliterated German strings to proper Umlauts in the dashboard (for example: "für", "prüfen", "Ferienüberschneidungen", "Vorfälle", "Ausfälle").
|
||||
|
||||
Notes for integrators:
|
||||
- On production boot, the active period is now derived from current date coverage automatically.
|
||||
- If customer calendars do not include today, startup logs a warning and dashboard banner will still guide admins to configure periods.
|
||||
|
||||
## 2026.1.0-alpha.15 (2026-03-31)
|
||||
- 🔌 **TV Power Intent Phase 1 (server-side)**:
|
||||
- Scheduler now publishes retained QoS1 group-level intents to `infoscreen/groups/{group_id}/power/intent`.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"appName": "Infoscreen-Management",
|
||||
"version": "2026.1.0-alpha.15",
|
||||
"version": "2026.1.0-alpha.16",
|
||||
"copyright": "© 2026 Third-Age-Applications",
|
||||
"supportContact": "support@third-age-applications.com",
|
||||
"description": "Eine zentrale Verwaltungsoberfläche für digitale Informationsbildschirme.",
|
||||
@@ -30,6 +30,16 @@
|
||||
"commitId": "9f2ae8b44c3a"
|
||||
},
|
||||
"changelog": [
|
||||
{
|
||||
"version": "2026.1.0-alpha.16",
|
||||
"date": "2026-04-02",
|
||||
"changes": [
|
||||
"✅ Dashboard: Der Ferienstatus-Banner zeigt die aktive akademische Periode jetzt zuverlässig nach Hard-Refresh und beim Wechsel zwischen Dashboard und Einstellungen.",
|
||||
"🧭 Navigation: Der Link vom Ferienstatus-Banner zu den Einstellungen bleibt stabil und funktioniert konsistent für Admin-Rollen.",
|
||||
"🚀 Deployment: Akademische Perioden werden nach Initialisierung automatisch für das aktuelle Datum aktiviert (kein manueller Aktivierungsschritt direkt nach Rollout mehr nötig).",
|
||||
"🔤 Sprache: Mehrere deutsche UI-Texte im Dashboard wurden auf korrekte Umlaute umgestellt (zum Beispiel für, prüfen, Vorfälle und Ausfälle)."
|
||||
]
|
||||
},
|
||||
{
|
||||
"version": "2026.1.0-alpha.15",
|
||||
"date": "2026-03-31",
|
||||
|
||||
@@ -19,8 +19,25 @@ export type PeriodUsage = {
|
||||
blockers: string[];
|
||||
};
|
||||
|
||||
function normalizeAcademicPeriod(period: any): AcademicPeriod {
|
||||
return {
|
||||
id: Number(period.id),
|
||||
name: period.name,
|
||||
displayName: period.displayName ?? period.display_name ?? null,
|
||||
startDate: period.startDate ?? period.start_date,
|
||||
endDate: period.endDate ?? period.end_date,
|
||||
periodType: period.periodType ?? period.period_type,
|
||||
isActive: Boolean(period.isActive ?? period.is_active),
|
||||
isArchived: Boolean(period.isArchived ?? period.is_archived),
|
||||
archivedAt: period.archivedAt ?? period.archived_at ?? null,
|
||||
archivedBy: period.archivedBy ?? period.archived_by ?? null,
|
||||
createdAt: period.createdAt ?? period.created_at,
|
||||
updatedAt: period.updatedAt ?? period.updated_at,
|
||||
};
|
||||
}
|
||||
|
||||
async function api<T>(url: string, init?: RequestInit): Promise<T> {
|
||||
const res = await fetch(url, { credentials: 'include', ...init });
|
||||
const res = await fetch(url, { credentials: 'include', cache: 'no-store', ...init });
|
||||
if (!res.ok) {
|
||||
const text = await res.text();
|
||||
try {
|
||||
@@ -35,10 +52,10 @@ async function api<T>(url: string, init?: RequestInit): Promise<T> {
|
||||
|
||||
export async function getAcademicPeriodForDate(date: Date): Promise<AcademicPeriod | null> {
|
||||
const iso = date.toISOString().slice(0, 10);
|
||||
const { period } = await api<{ period: AcademicPeriod | null }>(
|
||||
const { period } = await api<{ period: any | null }>(
|
||||
`/api/academic_periods/for_date?date=${iso}`
|
||||
);
|
||||
return period ?? null;
|
||||
return period ? normalizeAcademicPeriod(period) : null;
|
||||
}
|
||||
|
||||
export async function listAcademicPeriods(options?: {
|
||||
@@ -53,20 +70,20 @@ export async function listAcademicPeriods(options?: {
|
||||
params.set('archivedOnly', '1');
|
||||
}
|
||||
const query = params.toString();
|
||||
const { periods } = await api<{ periods: AcademicPeriod[] }>(
|
||||
const { periods } = await api<{ periods: any[] }>(
|
||||
`/api/academic_periods${query ? `?${query}` : ''}`
|
||||
);
|
||||
return Array.isArray(periods) ? periods : [];
|
||||
return Array.isArray(periods) ? periods.map(normalizeAcademicPeriod) : [];
|
||||
}
|
||||
|
||||
export async function getAcademicPeriod(id: number): Promise<AcademicPeriod> {
|
||||
const { period } = await api<{ period: AcademicPeriod }>(`/api/academic_periods/${id}`);
|
||||
return period;
|
||||
const { period } = await api<{ period: any }>(`/api/academic_periods/${id}`);
|
||||
return normalizeAcademicPeriod(period);
|
||||
}
|
||||
|
||||
export async function getActiveAcademicPeriod(): Promise<AcademicPeriod | null> {
|
||||
const { period } = await api<{ period: AcademicPeriod | null }>(`/api/academic_periods/active`);
|
||||
return period ?? null;
|
||||
const { period } = await api<{ period: any | null }>(`/api/academic_periods/active`);
|
||||
return period ? normalizeAcademicPeriod(period) : null;
|
||||
}
|
||||
|
||||
export async function createAcademicPeriod(payload: {
|
||||
@@ -76,12 +93,12 @@ export async function createAcademicPeriod(payload: {
|
||||
endDate: string;
|
||||
periodType: 'schuljahr' | 'semester' | 'trimester';
|
||||
}): Promise<AcademicPeriod> {
|
||||
const { period } = await api<{ period: AcademicPeriod }>(`/api/academic_periods`, {
|
||||
const { period } = await api<{ period: any }>(`/api/academic_periods`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
return period;
|
||||
return normalizeAcademicPeriod(period);
|
||||
}
|
||||
|
||||
export async function updateAcademicPeriod(
|
||||
@@ -94,36 +111,36 @@ export async function updateAcademicPeriod(
|
||||
periodType: 'schuljahr' | 'semester' | 'trimester';
|
||||
}>
|
||||
): Promise<AcademicPeriod> {
|
||||
const { period } = await api<{ period: AcademicPeriod }>(`/api/academic_periods/${id}`, {
|
||||
const { period } = await api<{ period: any }>(`/api/academic_periods/${id}`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
return period;
|
||||
return normalizeAcademicPeriod(period);
|
||||
}
|
||||
|
||||
export async function setActiveAcademicPeriod(id: number): Promise<AcademicPeriod> {
|
||||
const { period } = await api<{ period: AcademicPeriod }>(`/api/academic_periods/${id}/activate`, {
|
||||
const { period } = await api<{ period: any }>(`/api/academic_periods/${id}/activate`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
return period;
|
||||
return normalizeAcademicPeriod(period);
|
||||
}
|
||||
|
||||
export async function archiveAcademicPeriod(id: number): Promise<AcademicPeriod> {
|
||||
const { period } = await api<{ period: AcademicPeriod }>(`/api/academic_periods/${id}/archive`, {
|
||||
const { period } = await api<{ period: any }>(`/api/academic_periods/${id}/archive`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
return period;
|
||||
return normalizeAcademicPeriod(period);
|
||||
}
|
||||
|
||||
export async function restoreAcademicPeriod(id: number): Promise<AcademicPeriod> {
|
||||
const { period } = await api<{ period: AcademicPeriod }>(`/api/academic_periods/${id}/restore`, {
|
||||
const { period } = await api<{ period: any }>(`/api/academic_periods/${id}/restore`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
return period;
|
||||
return normalizeAcademicPeriod(period);
|
||||
}
|
||||
|
||||
export async function getAcademicPeriodUsage(id: number): Promise<PeriodUsage> {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -21,12 +21,14 @@ import {
|
||||
type PeriodUsage
|
||||
} from './apiAcademicPeriods';
|
||||
import { formatIsoDateForDisplay } from './dateFormatting';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Link, useLocation } from 'react-router-dom';
|
||||
|
||||
// Minimal event type for Syncfusion Tab 'selected' callback
|
||||
type TabSelectedEvent = { selectedIndex?: number };
|
||||
|
||||
const Einstellungen: React.FC = () => {
|
||||
const location = useLocation();
|
||||
|
||||
// Presentation settings state
|
||||
const [presentationInterval, setPresentationInterval] = React.useState(10);
|
||||
const [presentationPageProgress, setPresentationPageProgress] = React.useState(true);
|
||||
@@ -670,6 +672,8 @@ const Einstellungen: React.FC = () => {
|
||||
const isAdmin = !!(user && ['admin', 'superadmin'].includes(user.role));
|
||||
const isSuperadmin = !!(user && user.role === 'superadmin');
|
||||
|
||||
const [rootTabIndex, setRootTabIndex] = React.useState(0);
|
||||
|
||||
// Preserve selected nested-tab indices to avoid resets on parent re-render
|
||||
const [academicTabIndex, setAcademicTabIndex] = React.useState(0);
|
||||
const [displayTabIndex, setDisplayTabIndex] = React.useState(0);
|
||||
@@ -678,6 +682,22 @@ const Einstellungen: React.FC = () => {
|
||||
const [usersTabIndex, setUsersTabIndex] = React.useState(0);
|
||||
const [systemTabIndex, setSystemTabIndex] = React.useState(0);
|
||||
|
||||
React.useEffect(() => {
|
||||
const params = new URLSearchParams(location.search);
|
||||
const focus = params.get('focus');
|
||||
|
||||
if (focus === 'holidays') {
|
||||
setRootTabIndex(0);
|
||||
setAcademicTabIndex(1);
|
||||
return;
|
||||
}
|
||||
|
||||
if (focus === 'academic-periods') {
|
||||
setRootTabIndex(0);
|
||||
setAcademicTabIndex(0);
|
||||
}
|
||||
}, [location.search]);
|
||||
|
||||
// ---------- Leaf content functions (second-level tabs) ----------
|
||||
// Academic Calendar
|
||||
// (Old separate Import/List tab contents removed in favor of combined tab)
|
||||
@@ -1695,7 +1715,11 @@ const Einstellungen: React.FC = () => {
|
||||
|
||||
<h2 style={{ marginBottom: 20, fontSize: '24px', fontWeight: 600 }}>Einstellungen</h2>
|
||||
|
||||
<TabComponent heightAdjustMode="Auto">
|
||||
<TabComponent
|
||||
heightAdjustMode="Auto"
|
||||
selectedItem={rootTabIndex}
|
||||
selected={(e: TabSelectedEvent) => setRootTabIndex(e.selectedIndex ?? 0)}
|
||||
>
|
||||
<TabItemsDirective>
|
||||
<TabItemDirective header={{ text: '📅 Akademischer Kalender' }} content={AcademicCalendarTabs} />
|
||||
{isAdmin && (
|
||||
|
||||
@@ -94,6 +94,7 @@ services:
|
||||
command: >
|
||||
bash -c "alembic -c /app/server/alembic.ini upgrade head &&
|
||||
python /app/server/init_defaults.py &&
|
||||
python /app/server/init_academic_periods.py &&
|
||||
exec gunicorn server.wsgi:app --bind 0.0.0.0:8000"
|
||||
|
||||
dashboard:
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Erstellt Standard-Schuljahre für österreichische Schulen
|
||||
Führe dieses Skript nach der Migration aus, um Standard-Perioden zu erstellen.
|
||||
Erstellt Standard-Schuljahre und setzt automatisch die aktive Periode.
|
||||
|
||||
Dieses Skript ist idempotent:
|
||||
- Wenn keine Perioden existieren, werden Standard-Perioden erstellt.
|
||||
- Danach wird (bei jedem Lauf) die nicht-archivierte Periode aktiviert,
|
||||
die das heutige Datum abdeckt.
|
||||
"""
|
||||
|
||||
from datetime import date
|
||||
@@ -11,54 +15,94 @@ import sys
|
||||
sys.path.append('/workspace')
|
||||
|
||||
|
||||
def _create_default_periods_if_missing(session):
|
||||
"""Erstellt Standard-Schuljahre nur dann, wenn noch keine Perioden existieren."""
|
||||
existing = session.query(AcademicPeriod).first()
|
||||
if existing:
|
||||
print("Academic periods already exist. Skipping creation.")
|
||||
return False
|
||||
|
||||
periods = [
|
||||
{
|
||||
'name': 'Schuljahr 2024/25',
|
||||
'display_name': 'SJ 24/25',
|
||||
'start_date': date(2024, 9, 2),
|
||||
'end_date': date(2025, 7, 4),
|
||||
'period_type': AcademicPeriodType.schuljahr,
|
||||
'is_active': False
|
||||
},
|
||||
{
|
||||
'name': 'Schuljahr 2025/26',
|
||||
'display_name': 'SJ 25/26',
|
||||
'start_date': date(2025, 9, 1),
|
||||
'end_date': date(2026, 7, 3),
|
||||
'period_type': AcademicPeriodType.schuljahr,
|
||||
'is_active': False
|
||||
},
|
||||
{
|
||||
'name': 'Schuljahr 2026/27',
|
||||
'display_name': 'SJ 26/27',
|
||||
'start_date': date(2026, 9, 7),
|
||||
'end_date': date(2027, 7, 2),
|
||||
'period_type': AcademicPeriodType.schuljahr,
|
||||
'is_active': False
|
||||
}
|
||||
]
|
||||
|
||||
for period_data in periods:
|
||||
period = AcademicPeriod(**period_data)
|
||||
session.add(period)
|
||||
|
||||
session.flush()
|
||||
print(f"Successfully created {len(periods)} academic periods")
|
||||
return True
|
||||
|
||||
|
||||
def _activate_period_for_today(session):
|
||||
"""Aktiviert genau eine Periode: die Periode, die heute abdeckt."""
|
||||
today = date.today()
|
||||
|
||||
period_for_today = (
|
||||
session.query(AcademicPeriod)
|
||||
.filter(
|
||||
AcademicPeriod.is_archived == False,
|
||||
AcademicPeriod.start_date <= today,
|
||||
AcademicPeriod.end_date >= today,
|
||||
)
|
||||
.order_by(AcademicPeriod.start_date.desc())
|
||||
.first()
|
||||
)
|
||||
|
||||
# Immer zunächst alle aktiven Perioden deaktivieren, um den Zustand konsistent zu halten.
|
||||
session.query(AcademicPeriod).filter(AcademicPeriod.is_active == True).update(
|
||||
{AcademicPeriod.is_active: False},
|
||||
synchronize_session=False,
|
||||
)
|
||||
|
||||
if period_for_today:
|
||||
period_for_today.is_active = True
|
||||
print(
|
||||
f"Activated academic period for today ({today}): {period_for_today.name} "
|
||||
f"[{period_for_today.start_date} - {period_for_today.end_date}]"
|
||||
)
|
||||
else:
|
||||
print(
|
||||
f"WARNING: No academic period covers today ({today}). "
|
||||
"All periods remain inactive."
|
||||
)
|
||||
|
||||
|
||||
def create_default_academic_periods():
|
||||
"""Erstellt Standard-Schuljahre für österreichische Schulen"""
|
||||
"""Erstellt Standard-Perioden (falls nötig) und setzt aktive Periode für heute."""
|
||||
session = Session()
|
||||
|
||||
try:
|
||||
# Prüfe ob bereits Perioden existieren
|
||||
existing = session.query(AcademicPeriod).first()
|
||||
if existing:
|
||||
print("Academic periods already exist. Skipping creation.")
|
||||
return
|
||||
|
||||
# Standard Schuljahre erstellen
|
||||
periods = [
|
||||
{
|
||||
'name': 'Schuljahr 2024/25',
|
||||
'display_name': 'SJ 24/25',
|
||||
'start_date': date(2024, 9, 2),
|
||||
'end_date': date(2025, 7, 4),
|
||||
'period_type': AcademicPeriodType.schuljahr,
|
||||
'is_active': True # Aktuelles Schuljahr
|
||||
},
|
||||
{
|
||||
'name': 'Schuljahr 2025/26',
|
||||
'display_name': 'SJ 25/26',
|
||||
'start_date': date(2025, 9, 1),
|
||||
'end_date': date(2026, 7, 3),
|
||||
'period_type': AcademicPeriodType.schuljahr,
|
||||
'is_active': False
|
||||
},
|
||||
{
|
||||
'name': 'Schuljahr 2026/27',
|
||||
'display_name': 'SJ 26/27',
|
||||
'start_date': date(2026, 9, 7),
|
||||
'end_date': date(2027, 7, 2),
|
||||
'period_type': AcademicPeriodType.schuljahr,
|
||||
'is_active': False
|
||||
}
|
||||
]
|
||||
|
||||
for period_data in periods:
|
||||
period = AcademicPeriod(**period_data)
|
||||
session.add(period)
|
||||
|
||||
_create_default_periods_if_missing(session)
|
||||
_activate_period_for_today(session)
|
||||
session.commit()
|
||||
print(f"Successfully created {len(periods)} academic periods")
|
||||
|
||||
# Zeige erstellte Perioden
|
||||
for period in session.query(AcademicPeriod).all():
|
||||
for period in session.query(AcademicPeriod).order_by(AcademicPeriod.start_date.asc()).all():
|
||||
status = "AKTIV" if period.is_active else "inaktiv"
|
||||
print(
|
||||
f" - {period.name} ({period.start_date} - {period.end_date}) [{status}]")
|
||||
|
||||
Reference in New Issue
Block a user