Implement MVP: Anonymous feedback submission (User Story 1)
Complete implementation of Phase 1-3 (64 tasks): - Phase 1: Project setup with Flask, pytest, configuration - Phase 2: Core infrastructure (auth, models, services, testing) - Phase 3: Anonymous feedback submission with file uploads Features: - Anonymous feedback submission (text and/or up to 3 file attachments) - Multi-language support (any language accepted) - File validation (type, size) and virus scanning (ClamAV) - Product management with active/archived status - File-based storage with YAML metadata - User authentication system (Flask-Login) - CSRF protection and rate limiting - Test coverage: 10 passing tests (contract + integration) Security: - No IP address logging (FR-055 compliance) - File type whitelist and size limits (10MB max) - Virus scanning with graceful degradation - Filename sanitization and secure storage Test Results: - 8 contract tests passed - 2 integration tests passed - End-to-end workflow verified 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,100 @@
|
||||
"""Submission routes - anonymous feedback submission"""
|
||||
from flask import Blueprint, render_template, request, redirect, url_for, flash, abort
|
||||
from app.models.product import Product
|
||||
from app.services.feedback_storage import FeedbackStorageService
|
||||
from app.utils.file_validator import validate_file, scan_file_for_viruses
|
||||
|
||||
|
||||
bp = Blueprint('submission', __name__, url_prefix='/submit')
|
||||
|
||||
|
||||
@bp.route('/<product_slug>', methods=['GET'])
|
||||
def form(product_slug):
|
||||
"""Display feedback submission form
|
||||
|
||||
Args:
|
||||
product_slug: Product submission URL slug
|
||||
|
||||
Returns:
|
||||
Rendered submission form template or 404
|
||||
"""
|
||||
# Load product by slug
|
||||
product = Product.get_by_slug(product_slug)
|
||||
|
||||
if not product:
|
||||
abort(404, description="Product not found")
|
||||
|
||||
# Check if product is archived
|
||||
if product.is_archived():
|
||||
abort(404, description="This product is no longer accepting feedback")
|
||||
|
||||
return render_template('submission/form.html', product=product)
|
||||
|
||||
|
||||
@bp.route('/<product_slug>', 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:
|
||||
abort(404, description="Product not found")
|
||||
|
||||
# Check if product is archived
|
||||
if product.is_archived():
|
||||
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:
|
||||
abort(400, description="Please provide either feedback text or attachments")
|
||||
|
||||
# Validation: Maximum 3 files
|
||||
if len(files) > 3:
|
||||
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:
|
||||
abort(400, description=error_message)
|
||||
|
||||
# Scan for viruses
|
||||
is_clean, virus_message = scan_file_for_viruses(file)
|
||||
if not is_clean:
|
||||
abort(400, description=f"File rejected: {virus_message}")
|
||||
|
||||
# Save feedback
|
||||
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
|
||||
)
|
||||
|
||||
return render_template('submission/success.html',
|
||||
product=product,
|
||||
feedback_id=feedback.feedback_id)
|
||||
|
||||
except Exception as e:
|
||||
# Log error
|
||||
from flask import current_app
|
||||
current_app.logger.error(f"Error saving feedback: {e}")
|
||||
|
||||
return render_template('submission/error.html',
|
||||
product=product,
|
||||
error_message="An error occurred while saving your feedback. Please try again."), 500
|
||||
Reference in New Issue
Block a user