257 lines
7.5 KiB
Python
257 lines
7.5 KiB
Python
"""Feedback model"""
|
|||
|
|
import os
|
||
|
|
import uuid
|
||
|
|
from datetime import datetime
|
||
|
|
import yaml
|
||
|
|
from flask import current_app
|
||
|
|
|
||
|
|
|
||
|
|
class Feedback:
|
||
|
|
"""Feedback submission model
|
||
|
|
|
||
|
|
Attributes:
|
||
|
|
feedback_id: Unique feedback identifier (UUID)
|
||
|
|
product_id: Associated product ID
|
||
|
|
submitted_at: Submission timestamp (ISO 8601)
|
||
|
|
status: Feedback status ('new', 'analyzing', 'analyzed', 'analysis_failed', 'archived')
|
||
|
|
content_preview: First 200 chars of feedback text
|
||
|
|
has_attachments: Whether feedback has file attachments
|
||
|
|
attachment_count: Number of attached files
|
||
|
|
original_language: Detected language of feedback (set during analysis)
|
||
|
|
category: Feedback category (set during analysis)
|
||
|
|
"""
|
||
|
|
|
||
|
|
VALID_STATUSES = ['new', 'analyzing', 'analyzed', 'analysis_failed', 'archived']
|
||
|
|
|
||
|
|
def __init__(self, feedback_id, product_id, submitted_at=None, status='new',
|
||
|
|
content_preview='', has_attachments=False, attachment_count=0,
|
||
|
|
original_language=None, category=None):
|
||
|
|
self.feedback_id = feedback_id
|
||
|
|
self.product_id = product_id
|
||
|
|
self.submitted_at = submitted_at or datetime.utcnow().isoformat()
|
||
|
|
self.status = status
|
||
|
|
self.content_preview = content_preview
|
||
|
|
self.has_attachments = has_attachments
|
||
|
|
self.attachment_count = attachment_count
|
||
|
|
self.original_language = original_language
|
||
|
|
self.category = category
|
||
|
|
|
||
|
|
def to_dict(self):
|
||
|
|
"""Convert feedback to dictionary
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
dict: Feedback metadata
|
||
|
|
"""
|
||
|
|
data = {
|
||
|
|
'feedback_id': self.feedback_id,
|
||
|
|
'product_id': self.product_id,
|
||
|
|
'submitted_at': self.submitted_at,
|
||
|
|
'status': self.status,
|
||
|
|
'content_preview': self.content_preview,
|
||
|
|
'has_attachments': self.has_attachments,
|
||
|
|
'attachment_count': self.attachment_count
|
||
|
|
}
|
||
|
|
|
||
|
|
if self.original_language:
|
||
|
|
data['original_language'] = self.original_language
|
||
|
|
|
||
|
|
if self.category:
|
||
|
|
data['category'] = self.category
|
||
|
|
|
||
|
|
return data
|
||
|
|
|
||
|
|
@classmethod
|
||
|
|
def from_dict(cls, data):
|
||
|
|
"""Create feedback from dictionary
|
||
|
|
|
||
|
|
Args:
|
||
|
|
data: Dictionary with feedback data
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
Feedback: Feedback instance
|
||
|
|
"""
|
||
|
|
return cls(
|
||
|
|
feedback_id=data['feedback_id'],
|
||
|
|
product_id=data['product_id'],
|
||
|
|
submitted_at=data.get('submitted_at'),
|
||
|
|
status=data.get('status', 'new'),
|
||
|
|
content_preview=data.get('content_preview', ''),
|
||
|
|
has_attachments=data.get('has_attachments', False),
|
||
|
|
attachment_count=data.get('attachment_count', 0),
|
||
|
|
original_language=data.get('original_language'),
|
||
|
|
category=data.get('category')
|
||
|
|
)
|
||
|
|
|
||
|
|
@staticmethod
|
||
|
|
def generate_id():
|
||
|
|
"""Generate unique feedback ID
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
str: UUID-based feedback ID
|
||
|
|
"""
|
||
|
|
return str(uuid.uuid4())
|
||
|
|
|
||
|
|
@staticmethod
|
||
|
|
def _get_feedback_dir(product_id, feedback_id):
|
||
|
|
"""Get feedback directory path
|
||
|
|
|
||
|
|
Args:
|
||
|
|
product_id: Product ID
|
||
|
|
feedback_id: Feedback ID
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
str: Path to feedback directory
|
||
|
|
"""
|
||
|
|
return os.path.join(
|
||
|
|
current_app.config['DATA_DIR'],
|
||
|
|
'products',
|
||
|
|
product_id,
|
||
|
|
'feedback',
|
||
|
|
feedback_id
|
||
|
|
)
|
||
|
|
|
||
|
|
@staticmethod
|
||
|
|
def _get_metadata_file(product_id, feedback_id):
|
||
|
|
"""Get metadata file path
|
||
|
|
|
||
|
|
Args:
|
||
|
|
product_id: Product ID
|
||
|
|
feedback_id: Feedback ID
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
str: Path to metadata.yaml
|
||
|
|
"""
|
||
|
|
feedback_dir = Feedback._get_feedback_dir(product_id, feedback_id)
|
||
|
|
return os.path.join(feedback_dir, 'metadata.yaml')
|
||
|
|
|
||
|
|
@staticmethod
|
||
|
|
def _get_content_file(product_id, feedback_id):
|
||
|
|
"""Get content file path
|
||
|
|
|
||
|
|
Args:
|
||
|
|
product_id: Product ID
|
||
|
|
feedback_id: Feedback ID
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
str: Path to content.txt
|
||
|
|
"""
|
||
|
|
feedback_dir = Feedback._get_feedback_dir(product_id, feedback_id)
|
||
|
|
return os.path.join(feedback_dir, 'content.txt')
|
||
|
|
|
||
|
|
@staticmethod
|
||
|
|
def _get_attachments_dir(product_id, feedback_id):
|
||
|
|
"""Get attachments directory path
|
||
|
|
|
||
|
|
Args:
|
||
|
|
product_id: Product ID
|
||
|
|
feedback_id: Feedback ID
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
str: Path to attachments directory
|
||
|
|
"""
|
||
|
|
feedback_dir = Feedback._get_feedback_dir(product_id, feedback_id)
|
||
|
|
return os.path.join(feedback_dir, 'attachments')
|
||
|
|
|
||
|
|
@classmethod
|
||
|
|
def get_by_id(cls, product_id, feedback_id):
|
||
|
|
"""Load feedback by ID
|
||
|
|
|
||
|
|
Args:
|
||
|
|
product_id: Product ID
|
||
|
|
feedback_id: Feedback ID
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
Feedback or None: Feedback instance if found, None otherwise
|
||
|
|
"""
|
||
|
|
metadata_file = cls._get_metadata_file(product_id, feedback_id)
|
||
|
|
|
||
|
|
if not os.path.exists(metadata_file):
|
||
|
|
return None
|
||
|
|
|
||
|
|
with open(metadata_file, 'r') as f:
|
||
|
|
data = yaml.safe_load(f)
|
||
|
|
|
||
|
|
return cls.from_dict(data)
|
||
|
|
|
||
|
|
@classmethod
|
||
|
|
def get_all_for_product(cls, product_id):
|
||
|
|
"""Get all feedback for a product
|
||
|
|
|
||
|
|
Args:
|
||
|
|
product_id: Product ID
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
list: List of Feedback instances, sorted by submitted_at (newest first)
|
||
|
|
"""
|
||
|
|
feedback_list = []
|
||
|
|
feedback_base_dir = os.path.join(
|
||
|
|
current_app.config['DATA_DIR'],
|
||
|
|
'products',
|
||
|
|
product_id,
|
||
|
|
'feedback'
|
||
|
|
)
|
||
|
|
|
||
|
|
if not os.path.exists(feedback_base_dir):
|
||
|
|
return feedback_list
|
||
|
|
|
||
|
|
for feedback_id in os.listdir(feedback_base_dir):
|
||
|
|
feedback_dir = os.path.join(feedback_base_dir, feedback_id)
|
||
|
|
|
||
|
|
if not os.path.isdir(feedback_dir):
|
||
|
|
continue
|
||
|
|
|
||
|
|
feedback = cls.get_by_id(product_id, feedback_id)
|
||
|
|
if feedback:
|
||
|
|
feedback_list.append(feedback)
|
||
|
|
|
||
|
|
# Sort by submitted_at (newest first)
|
||
|
|
feedback_list.sort(key=lambda f: f.submitted_at, reverse=True)
|
||
|
|
|
||
|
|
return feedback_list
|
||
|
|
|
||
|
|
def save_metadata(self):
|
||
|
|
"""Save feedback metadata to filesystem"""
|
||
|
|
feedback_dir = self._get_feedback_dir(self.product_id, self.feedback_id)
|
||
|
|
os.makedirs(feedback_dir, exist_ok=True)
|
||
|
|
|
||
|
|
metadata_file = self._get_metadata_file(self.product_id, self.feedback_id)
|
||
|
|
|
||
|
|
with open(metadata_file, 'w') as f:
|
||
|
|
yaml.dump(self.to_dict(), f, default_flow_style=False)
|
||
|
|
|
||
|
|
def get_content(self):
|
||
|
|
"""Load feedback content text
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
str or None: Feedback content if exists, None otherwise
|
||
|
|
"""
|
||
|
|
content_file = self._get_content_file(self.product_id, self.feedback_id)
|
||
|
|
|
||
|
|
if not os.path.exists(content_file):
|
||
|
|
return None
|
||
|
|
|
||
|
|
with open(content_file, 'r') as f:
|
||
|
|
return f.read()
|
||
|
|
|
||
|
|
def get_attachments(self):
|
||
|
|
"""Get list of attachment filenames
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
list: List of attachment filenames
|
||
|
|
"""
|
||
|
|
attachments_dir = self._get_attachments_dir(self.product_id, self.feedback_id)
|
||
|
|
|
||
|
|
if not os.path.exists(attachments_dir):
|
||
|
|
return []
|
||
|
|
|
||
|
|
return [f for f in os.listdir(attachments_dir)
|
||
|
|
if os.path.isfile(os.path.join(attachments_dir, f))]
|
||
|
|
|
||
|
|
def validate_status(self):
|
||
|
|
"""Validate feedback status
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
bool: True if status is valid, False otherwise
|
||
|
|
"""
|
||
|
|
return self.status in self.VALID_STATUSES
|