"""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 @classmethod def load_active(cls): """Load all active products, sorted alphabetically by name then product_id Returns: list[Product]: Active products with valid submission_url_slug, sorted by: 1. name (case-insensitive alphabetical) 2. product_id (alphabetical) as tiebreaker Products with missing/invalid submission_url_slug are excluded. """ all_products = cls.get_all() active = [p for p in all_products if p.status == 'active' and p.submission_url_slug] return sorted(active, key=lambda p: (p.name.lower(), p.product_id)) 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'