"""Submission routes - anonymous feedback submission""" import threading import os from flask import Blueprint, render_template, request, redirect, url_for, flash, abort, current_app from app.models.product import Product from app.services.feedback_storage import FeedbackStorageService from app.services.ai_analyzer import ClaudeAnalyzer from app.utils.file_validator import validate_file, scan_file_for_viruses bp = Blueprint('submission', __name__, url_prefix='/submit') @bp.route('/', methods=['GET']) def form(product_slug): """Display feedback submission form Args: product_slug: Product submission URL slug Returns: Rendered submission form template or 404 """ try: # Load product by slug product = Product.get_by_slug(product_slug) if not product: current_app.logger.warning(f'Product not found: {product_slug}') abort(404, description="Product not found") # Check if product is archived if product.is_archived(): current_app.logger.info(f'Attempt to access archived product: {product_slug}') abort(404, description="This product is no longer accepting feedback") current_app.logger.info(f'Displaying submission form for product: {product_slug}') return render_template('submission/form.html', product=product) except Exception as e: current_app.logger.error(f'Error displaying submission form for {product_slug}: {e}', exc_info=True) abort(500) @bp.route('/', methods=['POST']) def submit(product_slug): """Process feedback submission Args: product_slug: Product submission URL slug Returns: Redirect to success page or error page """ # Load product by slug product = Product.get_by_slug(product_slug) if not product: current_app.logger.warning(f'Submission attempt for non-existent product: {product_slug}') abort(404, description="Product not found") # Check if product is archived if product.is_archived(): current_app.logger.warning(f'Submission attempt for archived product: {product_slug}') abort(404, description="This product is no longer accepting feedback") # Get form data feedback_text = request.form.get('feedback_text', '').strip() # Get uploaded files uploaded_files = request.files.getlist('files') # Filter out empty file inputs files = [f for f in uploaded_files if f and f.filename != ''] # Validation: Must provide either text or files if not feedback_text and not files: current_app.logger.info(f'Submission rejected: no content provided for {product_slug}') abort(400, description="Please provide either feedback text or attachments") # Validation: Maximum 3 files if len(files) > 3: current_app.logger.warning(f'Submission rejected: too many files ({len(files)}) for {product_slug}') abort(400, description="Maximum 3 attachments allowed") # Validate each file for file in files: is_valid, error_message = validate_file(file) if not is_valid: current_app.logger.warning(f'File validation failed for {product_slug}: {error_message}') abort(400, description=error_message) # Scan for viruses is_clean, virus_message = scan_file_for_viruses(file) if not is_clean: current_app.logger.warning(f'Virus scan failed for {product_slug}: {virus_message}') abort(400, description=f"File rejected: {virus_message}") # Save feedback (wrap only the save operation in try/except) try: feedback = FeedbackStorageService.save_complete_feedback( product_id=product.product_id, content_text=feedback_text if feedback_text else None, files=files if files else None ) current_app.logger.info(f'Feedback submitted successfully for {product_slug}: {feedback.feedback_id}') # Trigger background analysis (T084, T085) if feedback_text: # Only analyze if there's text content _trigger_background_analysis(feedback, feedback_text, product) return render_template('submission/success.html', product=product, feedback_id=feedback.feedback_id) except Exception as e: # Log error current_app.logger.error(f"Error saving feedback for {product_slug}: {e}", exc_info=True) return render_template('submission/error.html', product=product, error_message="An error occurred while saving your feedback. Please try again."), 500 def _trigger_background_analysis(feedback, feedback_text, product): """Trigger background AI analysis task (T084) Args: feedback: Feedback instance feedback_text: Feedback text content product: Product instance """ # Get the current app instance to pass to background thread app = current_app._get_current_object() # Run analysis in background thread thread = threading.Thread( target=_analyze_feedback_background, args=(app, feedback.product_id, feedback.feedback_id, feedback_text, product.owner_language) ) thread.daemon = True thread.start() def _analyze_feedback_background(app, product_id, feedback_id, feedback_text, target_language): """Background task for AI analysis (T086-T088) This runs in a separate thread to avoid blocking the submission response. Args: app: Flask app instance for application context product_id: Product ID feedback_id: Feedback ID feedback_text: Feedback text to analyze target_language: Target language for translation """ # Run within Flask application context with app.app_context(): try: # Update status to "analyzing" (T086) FeedbackStorageService.update_feedback_status_by_id( product_id, feedback_id, 'analyzing' ) # Get API key from app configuration api_key = current_app.config.get('ANTHROPIC_API_KEY') if not api_key: raise Exception("ANTHROPIC_API_KEY not configured in app settings") # Initialize analyzer analyzer = ClaudeAnalyzer(api_key=api_key) # Analyze feedback result = analyzer.analyze_feedback( feedback_text=feedback_text, target_language=target_language, product_id=product_id ) # Save analysis results FeedbackStorageService.save_analysis(product_id, feedback_id, result) # Update status to "analyzed" (T087) FeedbackStorageService.update_feedback_status_by_id( product_id, feedback_id, 'analyzed' ) except Exception as e: # Update status to "analysis_failed" on error (T088) FeedbackStorageService.update_feedback_status_by_id( product_id, feedback_id, 'analysis_failed' ) # Log error (T193) app.logger.error(f"Background analysis failed for feedback {feedback_id}: {e}", exc_info=True)