docs(settings): Update README + Copilot instructions; bump Program Info to 2025.1.0-alpha.11

README: Add System Settings API endpoints; describe new tabbed Settings layout with role gating; add Vite dev proxy tip to use relative /api paths.
Copilot instructions: Note SystemSetting key–value store in data model; document system_settings.py (CRUD + supplement-table convenience endpoint); reference apiSystemSettings.ts; note defaults seeding via init_defaults.py.
Program Info: Bump version to 2025.1.0-alpha.11; changelog explicitly tied to the Settings page (Events tab: supplement-table URL moved; Academic Calendar: set active period; proxy note); README docs mention.
No functional changes to API or UI code in this commit; documentation and program info only.
This commit is contained in:
RobbStarkAustria
2025-10-16 19:15:55 +00:00
parent 7b38b49598
commit 150937f2e2
11 changed files with 882 additions and 50 deletions

View File

@@ -0,0 +1,37 @@
"""add_system_settings_table
Revision ID: 045626c9719a
Revises: 488ce87c28ae
Create Date: 2025-10-16 18:38:47.415244
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = '045626c9719a'
down_revision: Union[str, None] = '488ce87c28ae'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
"""Upgrade schema."""
op.create_table(
'system_settings',
sa.Column('key', sa.String(100), nullable=False),
sa.Column('value', sa.Text(), nullable=True),
sa.Column('description', sa.String(255), nullable=True),
sa.Column('updated_at', sa.TIMESTAMP(timezone=True),
server_default=sa.func.current_timestamp(),
nullable=True),
sa.PrimaryKeyConstraint('key')
)
def downgrade() -> None:
"""Downgrade schema."""
op.drop_table('system_settings')

View File

@@ -42,3 +42,25 @@ with engine.connect() as conn:
print(f"✅ Superadmin-Benutzer '{admin_user}' angelegt.")
else:
print(f" Superadmin-Benutzer '{admin_user}' existiert bereits.")
# Default System Settings anlegen
default_settings = [
('supplement_table_url', '', 'URL für Vertretungsplan (Stundenplan-Änderungstabelle)'),
('supplement_table_enabled', 'false', 'Ob Vertretungsplan aktiviert ist'),
]
for key, value, description in default_settings:
result = conn.execute(
text("SELECT COUNT(*) FROM system_settings WHERE `key`=:key"),
{"key": key}
)
if result.scalar() == 0:
conn.execute(
text("INSERT INTO system_settings (`key`, value, description) VALUES (:key, :value, :description)"),
{"key": key, "value": value, "description": description}
)
print(f"✅ System-Einstellung '{key}' angelegt.")
else:
print(f" System-Einstellung '{key}' existiert bereits.")
print("✅ Initialisierung abgeschlossen.")

View File

@@ -122,24 +122,32 @@ def get_current_user():
db_session = Session()
try:
user = db_session.query(User).filter_by(id=user_id).first()
if not user:
# Session is stale, user was deleted
session.clear()
return jsonify({"error": "Not authenticated"}), 401
if not user.is_active:
# User was deactivated
session.clear()
return jsonify({"error": "Account is disabled"}), 401
# For SQLAlchemy Enum(UserRole), ensure we return the string value
role_value = user.role.value if isinstance(user.role, UserRole) else str(user.role)
return jsonify({
"id": user.id,
"username": user.username,
"role": user.role.value,
"role": role_value,
"is_active": user.is_active
}), 200
except Exception as e:
# Avoid naked 500s; return a JSON error with minimal info (safe in dev)
env = os.environ.get("ENV", "production").lower()
msg = str(e) if env in ("development", "dev") else "Internal server error"
return jsonify({"error": msg}), 500
finally:
db_session.close()

View File

@@ -0,0 +1,203 @@
"""
System Settings API endpoints.
Provides key-value storage for system-wide configuration.
"""
from flask import Blueprint, jsonify, request
from server.database import Session
from models.models import SystemSetting
from server.permissions import admin_or_higher
from sqlalchemy.exc import SQLAlchemyError
system_settings_bp = Blueprint('system_settings', __name__, url_prefix='/api/system-settings')
@system_settings_bp.route('', methods=['GET'])
@admin_or_higher
def get_all_settings():
"""
Get all system settings.
Admin+ only.
"""
session = Session()
try:
settings = session.query(SystemSetting).all()
return jsonify({
'settings': [s.to_dict() for s in settings]
}), 200
except SQLAlchemyError as e:
return jsonify({'error': str(e)}), 500
finally:
session.close()
@system_settings_bp.route('/<key>', methods=['GET'])
@admin_or_higher
def get_setting(key):
"""
Get a specific system setting by key.
Admin+ only.
"""
session = Session()
try:
setting = session.query(SystemSetting).filter_by(key=key).first()
if not setting:
return jsonify({'error': 'Setting not found'}), 404
return jsonify(setting.to_dict()), 200
except SQLAlchemyError as e:
return jsonify({'error': str(e)}), 500
finally:
session.close()
@system_settings_bp.route('/<key>', methods=['POST', 'PUT'])
@admin_or_higher
def update_setting(key):
"""
Create or update a system setting.
Admin+ only.
Request body:
{
"value": "string",
"description": "string" (optional)
}
"""
session = Session()
try:
data = request.get_json()
if not data:
return jsonify({'error': 'No data provided'}), 400
value = data.get('value')
description = data.get('description')
# Try to find existing setting
setting = session.query(SystemSetting).filter_by(key=key).first()
if setting:
# Update existing
setting.value = value
if description is not None:
setting.description = description
else:
# Create new
setting = SystemSetting(
key=key,
value=value,
description=description
)
session.add(setting)
session.commit()
return jsonify(setting.to_dict()), 200
except SQLAlchemyError as e:
session.rollback()
return jsonify({'error': str(e)}), 500
finally:
session.close()
@system_settings_bp.route('/<key>', methods=['DELETE'])
@admin_or_higher
def delete_setting(key):
"""
Delete a system setting.
Admin+ only.
"""
session = Session()
try:
setting = session.query(SystemSetting).filter_by(key=key).first()
if not setting:
return jsonify({'error': 'Setting not found'}), 404
session.delete(setting)
session.commit()
return jsonify({'message': 'Setting deleted successfully'}), 200
except SQLAlchemyError as e:
session.rollback()
return jsonify({'error': str(e)}), 500
finally:
session.close()
# Convenience endpoints for specific settings
@system_settings_bp.route('/supplement-table', methods=['GET'])
@admin_or_higher
def get_supplement_table_settings():
"""
Get supplement table URL and enabled status.
Admin+ only.
"""
session = Session()
try:
url_setting = session.query(SystemSetting).filter_by(key='supplement_table_url').first()
enabled_setting = session.query(SystemSetting).filter_by(key='supplement_table_enabled').first()
return jsonify({
'url': url_setting.value if url_setting else '',
'enabled': enabled_setting.value == 'true' if enabled_setting else False,
}), 200
except SQLAlchemyError as e:
return jsonify({'error': str(e)}), 500
finally:
session.close()
@system_settings_bp.route('/supplement-table', methods=['POST'])
@admin_or_higher
def update_supplement_table_settings():
"""
Update supplement table URL and enabled status.
Admin+ only.
Request body:
{
"url": "https://...",
"enabled": true/false
}
"""
session = Session()
try:
data = request.get_json()
if not data:
return jsonify({'error': 'No data provided'}), 400
url = data.get('url', '')
enabled = data.get('enabled', False)
# Update or create URL setting
url_setting = session.query(SystemSetting).filter_by(key='supplement_table_url').first()
if url_setting:
url_setting.value = url
else:
url_setting = SystemSetting(
key='supplement_table_url',
value=url,
description='URL für Vertretungsplan (Stundenplan-Änderungstabelle)'
)
session.add(url_setting)
# Update or create enabled setting
enabled_setting = session.query(SystemSetting).filter_by(key='supplement_table_enabled').first()
if enabled_setting:
enabled_setting.value = 'true' if enabled else 'false'
else:
enabled_setting = SystemSetting(
key='supplement_table_enabled',
value='true' if enabled else 'false',
description='Ob Vertretungsplan aktiviert ist'
)
session.add(enabled_setting)
session.commit()
return jsonify({
'url': url,
'enabled': enabled,
'message': 'Supplement table settings updated successfully'
}), 200
except SQLAlchemyError as e:
session.rollback()
return jsonify({'error': str(e)}), 500
finally:
session.close()

View File

@@ -9,6 +9,7 @@ from server.routes.academic_periods import academic_periods_bp
from server.routes.groups import groups_bp
from server.routes.clients import clients_bp
from server.routes.auth import auth_bp
from server.routes.system_settings import system_settings_bp
from server.database import Session, engine
from flask import Flask, jsonify, send_from_directory, request
import glob
@@ -23,6 +24,7 @@ app = Flask(__name__)
app.config['SECRET_KEY'] = os.environ.get('FLASK_SECRET_KEY', 'dev-secret-key-change-in-production')
app.config['SESSION_COOKIE_HTTPONLY'] = True
app.config['SESSION_COOKIE_SAMESITE'] = 'Lax'
app.register_blueprint(system_settings_bp)
# In production, set to True if using HTTPS
app.config['SESSION_COOKIE_SECURE'] = os.environ.get('ENV', 'development').lower() == 'production'
# Session lifetime: longer in development for convenience