2025-04-16 22:03:17 +02:00

364 lines
12 KiB
Python

"""Configuration management blueprint."""
from flask import Blueprint, render_template, redirect, url_for, request, flash, jsonify
from ..db import db
# Import the new model
from ..models import VolumeConfig, ScheduleConfig, ActivityLog, DownloadPathConfig
from ..defaults import MAX_VOLUME
import os # Import os for path validation
bp = Blueprint("config", __name__, url_prefix="/config")
# Helper functions for configuration updates
def _update_volume(new_volume):
"""
Helper function to update volume configuration.
Args:
new_volume (float): The new volume value
Returns:
tuple: (success, message, volume_config)
"""
try:
new_volume = float(new_volume)
if new_volume <= 0 or new_volume > MAX_VOLUME:
return False, f"Volume must be between 1 and {MAX_VOLUME}", None
volume_config = VolumeConfig.query.first()
if not volume_config:
volume_config = VolumeConfig(volume=new_volume)
db.session.add(volume_config)
else:
old_value = volume_config.volume
volume_config.volume = new_volume
ActivityLog.log_config_change(
config_key="scraper_volume",
old_value=old_value,
new_value=new_volume,
description="Updated scraper volume"
)
db.session.commit()
return True, "Volume updated successfully!", volume_config
except (ValueError, TypeError) as e:
db.session.rollback()
return False, f"Error updating volume: {str(e)}", None
# Add helper for download path
def _update_download_path(new_path):
"""
Helper function to update download path configuration.
Args:
new_path (str): The new download path
Returns:
tuple: (success, message, download_path_config)
"""
try:
# Basic validation: check if it's a non-empty string
if not new_path or not isinstance(new_path, str):
return False, "Download path cannot be empty.", None
# --- Add more validation like checking if path exists or is writable ---
# Check if the path exists and is a directory
if not os.path.isdir(new_path):
# Try to create it if it doesn't exist
try:
os.makedirs(new_path, exist_ok=True)
ActivityLog.log_system_activity(
action="create_directory",
status="info",
description=f"Created download directory: {new_path}"
)
except OSError as e:
ActivityLog.log_system_activity(
action="create_directory",
status="error",
description=f"Failed to create download directory: {new_path}, Error: {str(e)}"
)
return False, f"Path '{new_path}' is not a valid directory and could not be created: {e}", None
# Check if the path is writable
if not os.access(new_path, os.W_OK):
ActivityLog.log_system_activity(
action="check_directory_permissions",
status="error",
description=f"Download path '{new_path}' is not writable."
)
return False, f"Path '{new_path}' exists but is not writable by the application.", None
# --- End of validation ---
config = DownloadPathConfig.query.first()
if not config:
config = DownloadPathConfig(path=new_path)
db.session.add(config)
else:
old_value = config.path
config.path = new_path
ActivityLog.log_config_change(
config_key="download_path",
old_value=old_value,
new_value=new_path,
description="Updated download path"
)
db.session.commit()
return True, "Download path updated successfully!", config
except Exception as e:
db.session.rollback()
return False, f"Error updating download path: {str(e)}", None
def _update_schedule(schedule_data):
"""
Helper function to update schedule configuration.
Args:
schedule_data (dict): Dictionary with hour:weight pairs
Returns:
tuple: (success, message)
"""
try:
# Validate all entries first
for hour_str, weight in schedule_data.items():
try:
hour = int(hour_str)
weight = float(weight)
if hour < 0 or hour > 23:
return False, f"Hour value must be between 0 and 23, got {hour}"
if weight < 0.1 or weight > 5:
return False, f"Weight for hour {hour} must be between 0.1 and 5, got {weight}"
except ValueError:
return False, f"Invalid data format for hour {hour_str}"
# Update schedule after validation
for hour_str, weight in schedule_data.items():
hour = int(hour_str)
weight = float(weight)
config = ScheduleConfig.query.get(hour)
if not config:
config = ScheduleConfig(hour=hour, weight=weight)
db.session.add(config)
else:
old_value = config.weight
config.weight = weight
ActivityLog.log_config_change(
config_key=f"schedule_hour_{hour}",
old_value=old_value,
new_value=weight,
description=f"Updated schedule weight for hour {hour}"
)
db.session.commit()
return True, "Schedule updated successfully!"
except Exception as e:
db.session.rollback()
return False, f"Error updating schedule: {str(e)}"
@bp.route("/")
@bp.route("/general")
def general():
"""Show general configuration page."""
volume_config = VolumeConfig.query.first()
if not volume_config:
volume_config = VolumeConfig(volume=100) # Default value
db.session.add(volume_config)
db.session.commit()
# Fetch download path config
download_path_config = DownloadPathConfig.query.first()
if not download_path_config:
download_path_config = DownloadPathConfig() # Use default from model
db.session.add(download_path_config)
db.session.commit()
return render_template(
"config/index.html.jinja",
active_tab="general",
volume_config=volume_config,
download_path_config=download_path_config, # Pass to template
max_volume=MAX_VOLUME,
app_title="Configuration"
)
@bp.route("/schedule")
def schedule():
"""Show schedule configuration page."""
# Ensure we have schedule config for all hours
existing_hours = {record.hour: record for record in ScheduleConfig.query.all()}
schedule_config = {}
for hour in range(24):
if hour in existing_hours:
schedule_config[hour] = existing_hours[hour].weight
else:
# Create default schedule entry (weight 1.0)
new_config = ScheduleConfig(hour=hour, weight=1.0)
db.session.add(new_config)
schedule_config[hour] = 1.0
if len(existing_hours) < 24:
db.session.commit()
volume_config = VolumeConfig.query.first()
if not volume_config:
volume_config = VolumeConfig(volume=100) # Default value
db.session.add(volume_config)
db.session.commit()
return render_template(
"config/index.html.jinja",
active_tab="schedule",
schedule=schedule_config,
volume=volume_config.volume,
max_volume=MAX_VOLUME,
app_title="Configuration"
)
# Remove old update_volume route
# @bp.route("/update/volume", methods=["POST"])
# def update_volume(): ...
# Add new route to handle general settings form
@bp.route("/update/general", methods=["POST"])
def update_general():
"""Update general configuration (Volume and Download Path)."""
volume_success, volume_message = True, ""
path_success, path_message = True, ""
# Update Volume
new_volume = request.form.get("total_volume")
if new_volume is not None:
volume_success, volume_message, _ = _update_volume(new_volume)
if volume_success:
flash(volume_message, "success")
else:
flash(volume_message, "error")
# Update Download Path
new_path = request.form.get("download_path")
if new_path is not None:
path_success, path_message, _ = _update_download_path(new_path)
if path_success:
flash(path_message, "success")
else:
flash(path_message, "error")
return redirect(url_for("config.general"))
@bp.route("/update/schedule", methods=["POST"])
def update_schedule():
"""Update schedule configuration."""
schedule_data = {}
for hour in range(24):
key = f"hour_{hour}"
if key not in request.form:
flash(f"Missing data for hour {hour}", "error")
return redirect(url_for("config.schedule"))
schedule_data[str(hour)] = request.form.get(key, 0)
success, message = _update_schedule(schedule_data)
if success:
flash(message, "success")
else:
flash(message, "error")
return redirect(url_for("config.schedule"))
@bp.route("/api/schedule/stats")
def schedule_stats():
"""Get statistics about the current schedule configuration."""
volume_config = VolumeConfig.query.first()
if not volume_config:
return jsonify({"error": "No volume configuration found"})
total_volume = volume_config.volume
schedule_configs = ScheduleConfig.query.all()
if not schedule_configs:
return jsonify({"error": "No schedule configuration found"})
# Calculate total weight
total_weight = sum(config.weight for config in schedule_configs)
# Calculate papers per hour
papers_per_hour = {}
hourly_weights = {}
for config in schedule_configs:
weight_ratio = config.weight / total_weight if total_weight > 0 else 0
papers = weight_ratio * total_volume
papers_per_hour[config.hour] = papers
hourly_weights[config.hour] = config.weight
return jsonify({
"total_volume": total_volume,
"total_weight": total_weight,
"papers_per_hour": papers_per_hour,
"hourly_weights": hourly_weights
})
@bp.route("/api/update_config", methods=["POST"])
def api_update_config():
"""API endpoint to update configuration."""
data = request.json
response = {"success": True, "updates": []}
try:
# Update volume if provided
if "volume" in data:
success, message, _ = _update_volume(data["volume"])
response["updates"].append({
"type": "volume",
"success": success,
"message": message
})
if not success:
response["success"] = False
# Update download path if provided
if "download_path" in data:
success, message, _ = _update_download_path(data["download_path"])
response["updates"].append({
"type": "download_path",
"success": success,
"message": message
})
if not success:
response["success"] = False
# Update schedule if provided
if "schedule" in data:
success, message = _update_schedule(data["schedule"])
response["updates"].append({
"type": "schedule",
"success": success,
"message": message
})
if not success:
response["success"] = False
return jsonify(response)
except Exception as e:
db.session.rollback()
return jsonify({
"success": False,
"message": f"Unexpected error: {str(e)}"
})