Files
Reklamator/app/models/product.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

191 lines
5.2 KiB
Python

"""Product model"""
import os
import yaml
from flask import current_app
class Product:
"""Product/Service model
Attributes:
product_id: Unique product identifier
name: Product/service name
submission_url_slug: URL slug for submission form
owner_language: Preferred language for product owner
assigned_owner_ids: List of product owner user IDs
status: Product status ('active' or 'archived')
"""
def __init__(self, product_id, name, submission_url_slug, owner_language,
assigned_owner_ids, status='active'):
self.product_id = product_id
self.name = name
self.submission_url_slug = submission_url_slug
self.owner_language = owner_language
self.assigned_owner_ids = assigned_owner_ids or []
self.status = status
def to_dict(self):
"""Convert product to dictionary
Returns:
dict: Product data
"""
return {
'product_id': self.product_id,
'name': self.name,
'submission_url_slug': self.submission_url_slug,
'owner_language': self.owner_language,
'assigned_owner_ids': self.assigned_owner_ids,
'status': self.status
}
@classmethod
def from_dict(cls, data):
"""Create product from dictionary
Args:
data: Dictionary with product data
Returns:
Product: Product instance
"""
return cls(
product_id=data['product_id'],
name=data['name'],
submission_url_slug=data['submission_url_slug'],
owner_language=data['owner_language'],
assigned_owner_ids=data.get('assigned_owner_ids', []),
status=data.get('status', 'active')
)
@staticmethod
def _get_product_dir(product_id):
"""Get product directory path
Args:
product_id: Product ID
Returns:
str: Path to product directory
"""
return os.path.join(current_app.config['DATA_DIR'], 'products', product_id)
@staticmethod
def _get_config_file(product_id):
"""Get product config file path
Args:
product_id: Product ID
Returns:
str: Path to config.yaml
"""
product_dir = Product._get_product_dir(product_id)
return os.path.join(product_dir, 'config.yaml')
@classmethod
def get_by_id(cls, product_id):
"""Load product by ID
Args:
product_id: Product ID to load
Returns:
Product or None: Product instance if found, None otherwise
"""
config_file = cls._get_config_file(product_id)
if not os.path.exists(config_file):
return None
with open(config_file, 'r') as f:
data = yaml.safe_load(f)
return cls.from_dict(data)
@classmethod
def get_by_slug(cls, slug):
"""Load product by submission URL slug
Args:
slug: Submission URL slug
Returns:
Product or None: Product instance if found, None otherwise
"""
# Scan all product directories
products_dir = os.path.join(current_app.config['DATA_DIR'], 'products')
if not os.path.exists(products_dir):
return None
for product_id in os.listdir(products_dir):
product_dir = os.path.join(products_dir, product_id)
if not os.path.isdir(product_dir):
continue
config_file = os.path.join(product_dir, 'config.yaml')
if not os.path.exists(config_file):
continue
with open(config_file, 'r') as f:
data = yaml.safe_load(f)
if data.get('submission_url_slug') == slug:
return cls.from_dict(data)
return None
@classmethod
def get_all(cls):
"""Get all products
Returns:
list: List of Product instances
"""
products = []
products_dir = os.path.join(current_app.config['DATA_DIR'], 'products')
if not os.path.exists(products_dir):
return products
for product_id in os.listdir(products_dir):
product = cls.get_by_id(product_id)
if product:
products.append(product)
return products
def save(self):
"""Save product to filesystem"""
product_dir = self._get_product_dir(self.product_id)
os.makedirs(product_dir, exist_ok=True)
config_file = self._get_config_file(self.product_id)
with open(config_file, 'w') as f:
yaml.dump(self.to_dict(), f, default_flow_style=False)
def delete(self):
"""Delete product (not implemented - use archive instead)"""
raise NotImplementedError("Products should be archived, not deleted")
def is_active(self):
"""Check if product is active
Returns:
bool: True if status is 'active', False otherwise
"""
return self.status == 'active'
def is_archived(self):
"""Check if product is archived
Returns:
bool: True if status is 'archived', False otherwise
"""
return self.status == 'archived'