"""DOCX core properties (File → Info) for generated reports.""" from __future__ import annotations from dataclasses import dataclass from datetime import datetime from getpass import getuser from typing import Any CORE_PROP_MAX = 255 AUTHOR_SOURCES = ("os", "student", "custom") DEFAULT_AUTHOR_SOURCE = "os" DEFAULT_DOC_COMMENTS = "Создано при помощи md2gost (ТЗ МИРЭА)" def os_user() -> str: try: return (getuser() or "").strip() except Exception: return "" def clip_core(value: str | None) -> str: text = "" if value is None else str(value) if len(text) > CORE_PROP_MAX: return text[:CORE_PROP_MAX] return text def _normalize_source(value: str | None) -> str: src = (value or DEFAULT_AUTHOR_SOURCE).strip().lower() return src if src in AUTHOR_SOURCES else DEFAULT_AUTHOR_SOURCE @dataclass class DocumentMetadata: author_source: str = DEFAULT_AUTHOR_SOURCE author: str = "" last_modified_by: str = "" title: str = "" subject: str = "" keywords: str = "" comments: str = DEFAULT_DOC_COMMENTS category: str = "" def to_dict(self) -> dict[str, str]: return { "author_source": _normalize_source(self.author_source), "author": (self.author or "").strip(), "last_modified_by": (self.last_modified_by or "").strip(), "title": (self.title or "").strip(), "subject": (self.subject or "").strip(), "keywords": (self.keywords or "").strip(), "comments": "" if self.comments is None else str(self.comments).strip(), "category": (self.category or "").strip(), } @classmethod def from_dict(cls, raw: object) -> DocumentMetadata: if not isinstance(raw, dict): return cls() comments = raw.get("comments") if comments is None: comments_s = DEFAULT_DOC_COMMENTS else: comments_s = str(comments).strip() return cls( author_source=_normalize_source(str(raw.get("author_source") or "")), author=str(raw.get("author") or "").strip(), last_modified_by=str(raw.get("last_modified_by") or "").strip(), title=str(raw.get("title") or "").strip(), subject=str(raw.get("subject") or "").strip(), keywords=str(raw.get("keywords") or "").strip(), comments=comments_s, category=str(raw.get("category") or "").strip(), ) def _pick(override: str | None, fallback: str) -> str: if override is not None: return str(override) return fallback def resolve_document_metadata( *, stored: DocumentMetadata | None = None, student: str | None = None, author_source: str | None = None, author: str | None = None, last_modified_by: str | None = None, title: str | None = None, subject: str | None = None, keywords: str | None = None, comments: str | None = None, category: str | None = None, ) -> DocumentMetadata: """Merge CLI/request overrides over the saved profile into concrete core properties.""" base = stored or DocumentMetadata() src = _normalize_source(author_source or base.author_source) if author is not None: resolved_author = str(author).strip() elif src == "custom": resolved_author = (base.author or "").strip() elif src == "student": resolved_author = (student or "").strip() else: resolved_author = "" if not resolved_author: resolved_author = os_user() last_mod = _pick(last_modified_by, base.last_modified_by).strip() if not last_mod: last_mod = resolved_author return DocumentMetadata( author_source=src, author=resolved_author, last_modified_by=last_mod, title=_pick(title, base.title).strip(), subject=_pick(subject, base.subject).strip(), keywords=_pick(keywords, base.keywords).strip(), comments=_pick(comments, base.comments).strip(), category=_pick(category, base.category).strip(), ) def apply_document_metadata( document: Any, meta: DocumentMetadata, *, now: datetime | None = None, ) -> None: stamp = now or datetime.utcnow() if stamp.tzinfo is not None: stamp = stamp.replace(tzinfo=None) cp = document.core_properties cp.author = clip_core(meta.author) cp.last_modified_by = clip_core(meta.last_modified_by or meta.author) cp.title = clip_core(meta.title) cp.subject = clip_core(meta.subject) cp.keywords = clip_core(meta.keywords) cp.comments = clip_core(meta.comments) cp.category = clip_core(meta.category) cp.created = stamp cp.modified = stamp cp.revision = 1