"""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)}" })