Files
Reklamator/app/services/feedback_storage.py
T
gurixandClaude b301def134 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>
2025-10-16 15:14:51 +02:00

169 lines
4.6 KiB
Python

"""Feedback storage service"""
import os
import shutil
from flask import current_app
from app.models.feedback import Feedback
from app.utils.file_validator import get_safe_filename
class FeedbackStorageService:
"""Service for storing feedback to filesystem"""
@staticmethod
def create_feedback(product_id, content_text=None, files=None):
"""Create new feedback entry
Args:
product_id: Product ID
content_text: Feedback text content (optional)
files: List of uploaded files (optional)
Returns:
Feedback: Created feedback instance
"""
# Generate unique feedback ID
feedback_id = Feedback.generate_id()
# Create content preview (first 200 chars)
content_preview = ''
if content_text:
content_preview = content_text[:200]
# Check attachments
has_attachments = bool(files and len(files) > 0)
attachment_count = len(files) if files else 0
# Create feedback instance
feedback = Feedback(
feedback_id=feedback_id,
product_id=product_id,
status='new',
content_preview=content_preview,
has_attachments=has_attachments,
attachment_count=attachment_count
)
# Create directory structure
feedback_dir = Feedback._get_feedback_dir(product_id, feedback_id)
os.makedirs(feedback_dir, exist_ok=True)
return feedback
@staticmethod
def save_metadata(feedback):
"""Save feedback metadata to YAML file
Args:
feedback: Feedback instance to save
"""
feedback.save_metadata()
@staticmethod
def save_content(feedback, content_text):
"""Save feedback content to text file
Args:
feedback: Feedback instance
content_text: Feedback text content
"""
if not content_text:
return
content_file = Feedback._get_content_file(feedback.product_id, feedback.feedback_id)
with open(content_file, 'w', encoding='utf-8') as f:
f.write(content_text)
@staticmethod
def save_attachments(feedback, files):
"""Save attachment files
Args:
feedback: Feedback instance
files: List of Werkzeug FileStorage objects
Returns:
list: List of saved filenames
"""
if not files:
return []
attachments_dir = Feedback._get_attachments_dir(feedback.product_id, feedback.feedback_id)
os.makedirs(attachments_dir, exist_ok=True)
saved_files = []
for file in files:
if not file or file.filename == '':
continue
# Sanitize filename
safe_filename = get_safe_filename(file.filename)
# Save file
file_path = os.path.join(attachments_dir, safe_filename)
file.save(file_path)
saved_files.append(safe_filename)
return saved_files
@staticmethod
def save_complete_feedback(product_id, content_text=None, files=None):
"""Create and save complete feedback submission
Args:
product_id: Product ID
content_text: Feedback text content (optional)
files: List of uploaded files (optional)
Returns:
Feedback: Created and saved feedback instance
"""
# Create feedback
feedback = FeedbackStorageService.create_feedback(product_id, content_text, files)
# Save content
if content_text:
FeedbackStorageService.save_content(feedback, content_text)
# Save attachments
if files:
FeedbackStorageService.save_attachments(feedback, files)
# Save metadata
FeedbackStorageService.save_metadata(feedback)
return feedback
@staticmethod
def update_feedback_status(feedback, new_status):
"""Update feedback status
Args:
feedback: Feedback instance
new_status: New status value
Returns:
bool: True if updated successfully, False otherwise
"""
if new_status not in Feedback.VALID_STATUSES:
return False
feedback.status = new_status
feedback.save_metadata()
return True
@staticmethod
def delete_feedback(feedback):
"""Delete feedback and all associated files
Args:
feedback: Feedback instance to delete
"""
feedback_dir = Feedback._get_feedback_dir(feedback.product_id, feedback.feedback_id)
if os.path.exists(feedback_dir):
shutil.rmtree(feedback_dir)