From fab9e19962008640a05be450ffebc21d5fc38e24 Mon Sep 17 00:00:00 2001 From: Arseny <20096ninja@gmail.com> Date: Wed, 2 Sep 2026 03:29:54 +0300 Subject: [PATCH] =?UTF-8?q?[ADD]=20=D1=82=D0=B8=D1=82=D1=83=D0=BB=D1=8C?= =?UTF-8?q?=D0=BD=D1=8B=D0=B9=20=D0=BB=D0=B8=D1=81=D1=82=20=D0=BF=D0=BE=20?= =?UTF-8?q?=D0=B1=D0=BB=D0=B0=D0=BD=D0=BA=D1=83=20=D0=B7=D0=B0=D0=B4=D0=B0?= =?UTF-8?q?=D0=BD=D0=B8=D1=8F=20=D0=BD=D0=B0=20=D0=BF=D1=80=D0=B0=D0=BA?= =?UTF-8?q?=D1=82=D0=B8=D0=BA=D1=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- md2gost/latex/preamble.py | 2 +- md2gost/latex/titlepages/ptp_task.py | 139 ++++++++++++++++++++++++++- tests/test_render.py | 78 +++++++++++++++ 3 files changed, 217 insertions(+), 2 deletions(-) diff --git a/md2gost/latex/preamble.py b/md2gost/latex/preamble.py index 090d717..503c348 100644 --- a/md2gost/latex/preamble.py +++ b/md2gost/latex/preamble.py @@ -109,7 +109,7 @@ PREAMBLE = r"""\documentclass[a4paper, 14pt]{extarticle} \newcommand{\ptpfieldnote}[4][4.5cm]{% \ptpfield[#1]{#2}{#3}% - \par\vspace{-0.7em}% + \par\vspace{-0.45em}% \hspace*{#1}\makebox[\dimexpr\textwidth-#1\relax][c]{\footnotesize\itshape #4}\par} \newcommand{\ptpfieldpar}[3][4.5cm]{% diff --git a/md2gost/latex/titlepages/ptp_task.py b/md2gost/latex/titlepages/ptp_task.py index be92f23..09490b9 100644 --- a/md2gost/latex/titlepages/ptp_task.py +++ b/md2gost/latex/titlepages/ptp_task.py @@ -1,5 +1,142 @@ +"""Титульный лист по бланку «ЗАДАНИЕ на прохождение учебной практики». + +Бланк идёт отдельным листом и вкладывается в отчёт, поэтому документ состоит +из одного этого листа. Текст пунктов задания в модуле не хранится: у каждого +студента он свой и приходит из front matter ключами ``пункт_N``. +""" + from __future__ import annotations +import re + +from md2gost.handlers.inline import escape_latex +from md2gost.latex.titlepages._common import field, ministry_lines + +_ITEM_RE = re.compile(r"^пункт_(\d+)$") + + +def task_items(fm: dict[str, str]) -> list[str]: + """Пункты задания из ключей ``пункт_N``, отсортированные по номеру.""" + items = [ + (int(match.group(1)), value) + for key, value in fm.items() + if (match := _ITEM_RE.match(key)) and value.strip() + ] + if not items: + raise ValueError( + "front matter: у макета 'ptp-task' нет ни одного пункта задания; " + "добавьте поля 'пункт_1', 'пункт_2', ..." + ) + return [value for _, value in sorted(items)] + + +def _items_tex(fm: dict[str, str], team_line: str) -> str: + """Нумерованный список пунктов; под пунктом с двоеточием — строка команды.""" + lines: list[str] = [] + for text in task_items(fm): + lines.append(f" \\item {escape_latex(text)}") + if text.rstrip().endswith(":"): + lines.append( + " \\par\\vspace{0.5ex}" + f"\\noindent\\makebox[\\textwidth][c]{{\\itshape {team_line}}}" + "\\par\\vspace{-0.6em}" + "\\noindent\\rule{\\textwidth}{0.4pt}\\par" + ) + return "\n".join(lines) + def render(fm: dict[str, str]) -> str: - raise NotImplementedError + department = field(fm, "кафедра") + dep_index = field(fm, "кафедра_индекс", "ИУ9") + student_full = field(fm, "студент_полностью") or field(fm, "студент") + group = field(fm, "группа") + company = field(fm, "предприятие") + team = field(fm, "команда") + repo = field(fm, "хранилище") + advisor = field(fm, "руководитель") + day = field(fm, "дата_выдачи_день") + month = field(fm, "дата_выдачи_месяц") + year = field(fm, "дата_выдачи_год", "2026") + + team_line = f"{team} (хранилище {repo})" if repo else team + student_line = f"{student_full}, {group}" if group else student_full + + return ( + "\\begin{titlepage}\n" + "\\thispagestyle{empty}\n" + "\\setstretch{1.0}\n" + # center/flushright добавляют \\topsep и \\parskip сверху и снизу; + # на бланке из-за этого не остаётся места под подписи. + "\\setlength{\\parskip}{0pt}\n" + "\\setlength{\\topsep}{0pt}\n" + "\\setlength{\\partopsep}{0pt}\n" + "\n" + "% --- Шапка без эмблемы: в бланке задания её нет ---\n" + "\\vspace*{-52pt}\n" + "{\\small\\bfseries\n" + f"{ministry_lines(fm)}}}\n" + "\n" + "% --- Полосы ---\n" + "\\vspace{1ex}\n" + "\\noindent\\rule{\\textwidth}{2.3pt}\n" + "\n" + "\\vspace*{-18pt}\n" + "\\noindent\\rule{\\textwidth}{0.4pt}\n" + "\n" + "% --- Кафедра ---\n" + "\\vspace{1ex}\n" + "\\begin{center}\n" + f"Кафедра <<\\underline{{{department}}}>> (\\underline{{{dep_index}}})\n" + "\\end{center}\n" + "\n" + "% --- Утверждаю ---\n" + "\\vspace{0.5ex}\n" + "\\begin{flushright}\n" + "УТВЕРЖДАЮ \\\\[0.4ex]\n" + "Заведующий кафедрой \\rule{3cm}{0.4pt} \\\\[1ex]\n" + "\\rule{3.2cm}{0.4pt} \\quad \\rule{4cm}{0.4pt} \\\\[1ex]\n" + "<<\\,\\rule{1cm}{0.4pt}\\,>> \\rule{3cm}{0.4pt} 20\\,\\rule{0.8cm}{0.4pt} г.\n" + "\\end{flushright}\n" + "\n" + "% --- Заголовок ---\n" + "\\vspace{0.5ex}\n" + "\\begin{center}\n" + "{\\large\\bfseries З\\,А\\,Д\\,А\\,Н\\,И\\,Е} \\\\[0.4ex]\n" + "{\\bfseries на прохождение учебной практики}\n" + "\\end{center}\n" + "\n" + "% --- Поля ---\n" + "\\vspace{1ex}\n" + f"\\ptpfield[3.3cm]{{на предприятии}}{{{company}}}\n" + "\n" + "\\vspace{0.8ex}\n" + f"\\ptpfieldnote[2.2cm]{{Студент}}{{{student_line}}}" + "{(фамилия, имя, отчество; инициалы; индекс группы)}\n" + "\n" + "% --- Пункты задания ---\n" + "\\vspace{1.2ex}\n" + "\\noindent Во время прохождения учебной практики студент должен:\n" + # Пункты набираются мельче основного кегля — как в бланке кафедры, + # иначе полный список не помещается на лист вместе с подписями. + "{\\small\n" + "\\begin{enumerate}[wide=0pt, labelindent=0pt, labelwidth=0pt,\n" + " label=\\arabic*., itemsep=0.2ex, topsep=0.3ex, parsep=0pt]\n" + f"{_items_tex(fm, team_line)}\n" + "\\end{enumerate}}\n" + "\n" + "% --- Дата выдачи ---\n" + "\\vspace{1ex}\n" + "\\noindent Дата выдачи задания " + f"<<\\,\\underline{{~{day}~}}\\,>> \\underline{{~{month}~}} " + f"\\underline{{~{year}~}} г.\n" + "\n" + "% --- Подписи ---\n" + "\\vspace{0.6em}\n" + f"{{\\setstretch{{1.3}}\\ptpsig{{\\bfseries Руководитель практики от кафедры}}" + f"{{{advisor}}}{{(Фамилия И.О.)}}}}\n" + "\n" + "\\vspace{0.5em}\n" + "{\\setstretch{1.3}\\ptpsig{\\bfseries Студент}{}{(Фамилия И.О.)}}\n" + "\n" + "\\end{titlepage}" + ) diff --git a/tests/test_render.py b/tests/test_render.py index 5f441f5..0a298ee 100644 --- a/tests/test_render.py +++ b/tests/test_render.py @@ -231,3 +231,81 @@ def test_ptp_report_has_no_hardcoded_names() -> None: source = inspect.getsource(ptp_report) for forbidden in ("Посевин", "Коновалов", "Камалетдинов", "ИУ9-2"): assert forbidden not in source + + +PTP_TASK_FM = ( + "титульник: ptp-task\n" + "кафедра: Теоретическая информатика и компьютерные технологии\n" + "кафедра_индекс: ИУ9\n" + "студент: Булдаков А. С.\n" + "студент_полностью: Булдаков Арсений Сергеевич\n" + "группа: ИУ9-22Б\n" + "предприятие: МГТУ имени Н. Э. Баумана\n" + "команда: Система мониторинга радиоэфира\n" + "хранилище: https://monitor.yss.su/IU9/radio-serv\n" + "руководитель: Посевин Д. П.\n" + "дата_выдачи_день: 29\n" + "дата_выдачи_месяц: июня\n" + "дата_выдачи_год: 2026\n" + "пункт_1: Изучить принципы построения систем пеленгации.\n" + "пункт_2: Получить задание в составе команды:\n" + "пункт_10: Написать отчёт.\n" +) + + +def test_ptp_task_renders_items_in_numeric_order() -> None: + """Пункт 10 идёт после пункта 2, а не между 1 и 2 как при сортировке строк.""" + tex = render_fm(PTP_TASK_FM) + first = tex.index("Изучить принципы построения систем пеленгации.") + second = tex.index("Получить задание в составе команды:") + third = tex.index("Написать отчёт.") + assert first < second < third + + +def test_ptp_task_requires_items() -> None: + with pytest.raises(ValueError, match="пункт_1"): + render_fm("титульник: ptp-task\nстудент: Булдаков А. С.\n") + + +def test_ptp_task_has_blank_furniture() -> None: + tex = render_fm(PTP_TASK_FM) + for fragment in ( + "УТВЕРЖДАЮ", + "Заведующий кафедрой", + "З\\,А\\,Д\\,А\\,Н\\,И\\,Е", + "на прохождение учебной практики", + "Дата выдачи задания", + "Руководитель практики от кафедры", + ): + assert fragment in tex + + +def test_ptp_task_student_line_carries_group() -> None: + tex = render_fm(PTP_TASK_FM) + assert "Булдаков Арсений Сергеевич, ИУ9-22Б" in tex + + +def test_ptp_task_team_line_follows_item_with_colon() -> None: + tex = render_fm(PTP_TASK_FM) + colon_item = tex.index("Получить задание в составе команды:") + team = tex.index("Система мониторинга радиоэфира (хранилище") + next_item = tex.index("Написать отчёт.") + assert colon_item < team < next_item + + +def test_ptp_task_has_no_emblem() -> None: + """В бланке задания эмблемы нет.""" + tex = render_fm(PTP_TASK_FM) + titlepage = tex[tex.index("\\begin{titlepage}") : tex.index("\\end{titlepage}")] + assert "emblem" not in titlepage + + +def test_ptp_task_has_no_hardcoded_assignment_text() -> None: + """Пункты задания и имена приходят только из front matter.""" + import inspect + + from md2gost.latex.titlepages import ptp_task + + source = inspect.getsource(ptp_task) + for forbidden in ("Web", "2D-игра", "GitFlic", "Javascript", "Посевин", "Коновалов"): + assert forbidden not in source