-
{% for a in job.achievements %}
@@ -73,11 +78,11 @@
From d5ff05abdb9f79ddb2a47d383d75e9f4aaabeed7 Mon Sep 17 00:00:00 2001 From: Pavel Sobolev
{data['summary']}
") - meta_tags = [] - if data["location"]: + def _build_print_css(self) -> str: + """Вернуть минимальный CSS для печатного PDF. + + Returns: + str: Строка CSS-правил, совместимых с WeasyPrint. + """ + css_rules = """ + @page { size: A4; margin: 18mm 16mm; } + body { + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", + Roboto, Arial, sans-serif; + color: #111; + } + h1 { + margin: 0 0 6mm 0; + font-size: 20pt; + } + h2 { + margin: 8mm 0 3mm 0; + font-size: 13pt; + border-bottom: 1px solid #ccc; + padding-bottom: 2mm; + } + p { + margin: 0 0 3mm 0; + line-height: 1.4; + } + .meta { color: #555; } + .tag { + display: inline-block; + border: 1px solid #ddd; + padding: 2px 6px; + border-radius: 10px; + font-size: 9pt; + color: #444; + margin-right: 4px; + } + .item { margin: 0 0 5mm 0; } + .item-title { font-weight: 700; } + .item-period { color: #666; } + ul { margin: 2mm 0 2mm 6mm; } + """.strip() + + return css_rules + + def _append_head(self, full_name: str) -> None: + """Добавить секцию head с CSS. + + Args: + full_name (str): Полное имя для тега{data['summary']}
") + meta_tags: list[str] = [] + if data.get("location"): meta_tags.append(f"{data['location']}") - if data["languages"]: + if data.get("languages"): meta_tags.append(f"Языки: {', '.join(data['languages'])}") if meta_tags: - parts.append(f"") + self.parts.append(f"") - # Contacts - contacts = data["contacts"] - parts.append("") + def _append_contacts_section(self, contacts: dict[str, Any]) -> None: + """Добавить секцию контактов.""" + self.parts.append("
")
if contacts.get("email"):
- parts.append(f"Email: {contacts['email']}
")
+ self.parts.append(f"Email: {contacts['email']}
")
if contacts.get("phone"):
- parts.append(f"Телефон: {contacts['phone']}
")
+ self.parts.append(f"Телефон: {contacts['phone']}
")
if contacts.get("telegram"):
- parts.append(f"Telegram: {contacts['telegram']}")
- parts.append("
{e.summary}
") + self.parts.append(f"{e.summary}
") if e.achievements: - parts.append("{g.group}: {', '.join([i for i in g.items if i])}
") + self.parts.append( + f"{g.group}: {', '.join([i for i in g.items if i])}
" + ) else: - parts.append("") + self.parts.append("") - parts.append("") + def render(self, profile: Profile) -> BytesIO: + """Формирует HTML на лету (без шаблона) и конвертирует в PDF. - html = "".join(parts) + Args: + profile (Profile): Профиль для сериализации. + + Returns: + BytesIO: PDF-файл. + """ + logger.info("Старт рендеринга PDF для профиля") + data = self.serializer.serialize(profile) + if not data or "full_name" not in data or not data["full_name"]: + logger.error("Отсутствует обязательное поле full_name после сериализации") + raise ValueError("Невозможно сформировать PDF: отсутствует full_name") + + logger.info("Сериализация профиля завершена, подготовка HTML") + # Head + Header + self._append_head(data["full_name"]) + self._append_header_section(data) + # Contacts + self._append_contacts_section(data["contacts"]) + # Experience + self._append_experience_section(data["experience"]) + # Skills + self._append_skills_section(data["skills_map"]) + + self.parts.append("") + + logger.info("HTML подготовлен, генерация PDF через WeasyPrint") + html = "".join(self.parts) pdf = HTML(string=html).write_pdf() out = BytesIO(pdf) out.seek(0) + logger.info("PDF сгенерирован: %s байт", len(pdf)) return out - - diff --git a/cv/templates/index.html b/cv/templates/index.html index 5082d06..e858d89 100644 --- a/cv/templates/index.html +++ b/cv/templates/index.html @@ -35,8 +35,13 @@{{ profile.role }} • {{ profile.location }}
{% endif %} -{{ profile.summary }}
+ {% if profile.role %} + + {% endif %} +{{ profile.summary }}