Files
resume/cv/services/dowload/docx.py
Pavel Sobolev d5ff05abdb add linters
2025-11-13 01:33:00 +03:00

163 lines
6.2 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Рендеринг резюме в формат 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