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:
@@ -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')
|
||||
@@ -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.")
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
203
server/routes/system_settings.py
Normal file
203
server/routes/system_settings.py
Normal 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()
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user