"""Рендеринг резюме в формат DOCX. Класс `DocxRenderer` формирует документ с заголовками, контактами, опытом и навыками на основе сериализованных данных профиля. """ from __future__ import annotations import logging from collections.abc import Sequence from io import BytesIO from typing import Any from docx import Document as new_document from docx.document import Document as DocxDocument from docx.oxml import OxmlElement from docx.oxml.ns import qn from docx.shared import Pt, RGBColor from cv.services.dowload.base import ProfileSerializer from resume.utils.logging import configure_root_logger _root = logging.getLogger() if not _root.handlers: configure_root_logger() logger = logging.getLogger(__name__) class DocxRenderer: """Создает DOCX-документ на основе данных профиля.""" def __init__( self, ) -> None: """Инициализирует сериализатор документов DOCX.""" self.serializer = ProfileSerializer() @staticmethod def _add_divider(document: DocxDocument) -> None: """Добавить горизонтальный разделитель как границу параграфа.""" p = document.add_paragraph() fmt = p.paragraph_format fmt.space_before = Pt(6) fmt.space_after = Pt(6) p_elm = p._p pPr = p_elm.get_or_add_pPr() pBdr = OxmlElement("w:pBdr") bottom = OxmlElement("w:bottom") bottom.set(qn("w:val"), "single") bottom.set(qn("w:sz"), "6") bottom.set(qn("w:space"), "1") bottom.set(qn("w:color"), "2a3347") pBdr.append(bottom) pPr.append(pBdr) @staticmethod def _month_year(dt: Any) -> str: """Вернуть строку месяца и года на русском для даты.""" ru_months = { 1: "январь", 2: "февраль", 3: "март", 4: "апрель", 5: "май", 6: "июнь", 7: "июль", 8: "август", 9: "сентябрь", 10: "октябрь", 11: "ноябрь", 12: "декабрь", } return f"{ru_months.get(dt.month, dt.strftime('%B')).capitalize()} {dt.year}" def _append_header(self, doc: DocxDocument, data: dict[str, Any]) -> None: """Добавить заголовок и краткие сведения.""" doc.core_properties.title = data["full_name"] if data.get("role"): doc.core_properties.subject = data["role"] doc.add_heading(data["full_name"], level=0) if data.get("role"): doc.add_paragraph(data["role"]) if data.get("summary"): doc.add_paragraph(data["summary"]) meta_parts: list[str] = [] if data.get("location"): meta_parts.append(data["location"]) if data.get("languages"): meta_parts.append("Языки: " + ", ".join(data["languages"])) if meta_parts: doc.add_paragraph(" • ".join(meta_parts)) def _append_contacts(self, doc: DocxDocument, contacts: dict[str, Any]) -> None: """Добавить секцию контактов.""" doc.add_heading("Контакты", level=1) if contacts.get("email"): doc.add_paragraph(f"Email: {contacts['email']}") if contacts.get("phone"): doc.add_paragraph(f"Телефон: {contacts['phone']}") if contacts.get("telegram"): doc.add_paragraph(f"Telegram: {contacts['telegram']}") def _append_experience(self, doc: DocxDocument, experience: Sequence[Any] | None) -> None: """Добавить опыт работы.""" doc.add_heading("Опыт работы", level=1) for i, e in enumerate(experience or []): if i: self._add_divider(doc) start = self._month_year(e.start_date) if getattr(e, "start_date", None) else "" end = ( self._month_year(e.end_date) if getattr(e, "end_date", None) else "настоящее время" ) period = f"{start} — {end}" p_company = doc.add_paragraph() run_company = p_company.add_run(e.company) run_company.bold = True run_company.font.underline = True run_company.font.color.rgb = RGBColor(0x2A, 0x33, 0x47) p_period = doc.add_paragraph(period) for r in p_period.runs: r.font.color.rgb = RGBColor(0x99, 0xA2, 0xB2) if e.summary: doc.add_paragraph(e.summary) for a in e.achievements or []: if a: doc.add_paragraph(a, style="List Bullet") if e.tech: doc.add_paragraph("Технологии: " + ", ".join([t for t in e.tech if t])) def _append_skills(self, doc: DocxDocument, skills: Sequence[Any] | None) -> None: """Добавить навыки.""" doc.add_heading("Навыки", level=1) for g in skills or []: if g.items: doc.add_paragraph(f"{g.group}: " + ", ".join([i for i in g.items if i])) def render(self, profile: Any) -> BytesIO: """Сформировать DOCX документ по профилю. Args: profile: Объект профиля. Returns: BytesIO: Буфер с содержимым DOCX. """ logger.info("Старт рендеринга DOCX для профиля") data = self.serializer.serialize(profile) buf = BytesIO() doc: DocxDocument = new_document() # Заголовок self._append_header(doc, data) # Контакты self._append_contacts(doc, data.get("contacts", {})) # Опыт self._append_experience(doc, data.get("experience")) # Навыки self._append_skills(doc, data.get("skills_map")) doc.save(buf) buf.seek(0) logger.info("DOCX сгенерирован: %s байт", buf.getbuffer().nbytes) return buf