From 00100a8406d2e6bed9f2484659e95aca80e9116a Mon Sep 17 00:00:00 2001 From: ev-osi Date: Wed, 23 Jul 2025 04:15:30 +0300 Subject: [PATCH 1/2] Added checking of LaTex reports --- grading.py | 111 +++++++++++++++++++++++++++++++++++++++++++++++++++++ main.py | 40 +++++++++++++++++++ 2 files changed, 151 insertions(+) create mode 100644 grading.py diff --git a/grading.py b/grading.py new file mode 100644 index 0000000..69852b5 --- /dev/null +++ b/grading.py @@ -0,0 +1,111 @@ +import requests +import re +from io import StringIO + + +def extract_latex_content(text): + """Парсер LaTeX-контента с обработкой таблиц и спецформатирования""" + # Удаление комментариев + text = re.sub(r'%.*', '', text) + + # Извлечение содержимого между \begin{document} и \end{document} + doc_match = re.search(r'\\begin{document}(.*?)\\end{document}', text, re.DOTALL) + if not doc_match: + return "" + content = doc_match.group(1) + + # Обработка таблиц - извлечение текста из ячеек + content = re.sub(r'\\begin{tabular}.*?\\end{tabular}', + lambda m: ' '.join(re.findall(r'\{([^{}]*)\}', m.group())), + content, flags=re.DOTALL) + + # Удаление LaTeX-команд с сохранением их аргументов + content = re.sub(r'\\[a-zA-Z]+\*?\s*\{([^{}]*)\}', r'\1', content) + content = re.sub(r'\\[a-zA-Z]+\*?\s*', ' ', content) + + # Удаление оставшихся спецсимволов + content = re.sub(r'[{}]', '', content) + content = re.sub(r'\\[^\s]*', '', content) + + # Нормализация пробелов + content = ' '.join(content.split()) + return content + +def extract_titlepage_content(text): + """Извлекает содержимое титульного листа""" + # Удаление комментариев + text = re.sub(r'%.*', '', text) + + # Извлечение содержимого между \begin{titlepage} и \end{titlepage} + titlepage_match = re.search(r'\\begin{titlepage}(.*?)\\end{titlepage}', text, re.DOTALL) + if not titlepage_match: + return "" + content = titlepage_match.group(1) + + # Обработка содержимого (аналогично extract_latex_content) + content = re.sub(r'\\[a-zA-Z]+\*?\s*\{([^{}]*)\}', r'\1', content) + content = re.sub(r'\\[a-zA-Z]+\*?\s*', ' ', content) + content = re.sub(r'[{}]', '', content) + content = re.sub(r'\\[^\s]*', '', content) + content = ' '.join(content.split()) + + return content + +def convert_fullname_to_initials(fullname): + """Преобразует 'Фамилия Имя Отчество' в 'И.О.Фамилия'""" + parts = fullname.split() + if len(parts) == 3: + surname, name, patronymic = parts + return f"{name[0]}.{patronymic[0]}.{surname}" + return fullname + +def grade_report_latex(repo, filename, subject, lab_name, group, student_name, required_sections, branch=None, commit=None, github_token=None): + """Улучшенная функция проверки LaTeX-отчета""" + # Скачивание файла + ref = commit if commit else branch if branch else 'main' + api_url = f"https://api.github.com/repos/{repo}/contents/{filename}?ref={ref}" + + try: + # Заголовки для GitHub API + headers = { + 'Accept': 'application/vnd.github.v3+json', + } + if github_token: + headers['Authorization'] = f'token {github_token}' + + response = requests.get(api_url, headers=headers) + response.raise_for_status() + + # Декодируем содержимое файла из base64 + import base64 + content = base64.b64decode(response.json()['content']).decode('utf-8') + + except Exception as e: + raise Exception(f"Ошибка при загрузке файла через GitHub API: {str(e)}") + + # Извлечение содержимого титульного листа + titlepage_text = extract_titlepage_content(content) + + # Извлечение основного содержимого + full_text = extract_latex_content(content) + + # Преобразование 'Фамилия Имя Отчество' в 'И.О.Фамилия' + student_name = convert_fullname_to_initials(student_name) + + # Проверка титульной информации (только в titlepage) + title_checks = { + "subject": subject.lower() in titlepage_text.lower(), + "lab_name": lab_name.lower() in titlepage_text.lower(), + "group": str(group).lower() in titlepage_text.lower(), + "student_name": student_name.lower() in titlepage_text.lower().replace(" ", "") + } + + # Проверка разделов (ищем как \section{Name}, так и \section*{Name}) + sections = re.findall(r'\\section\*?\{([^{}]*)\}', content) + section_checks = {sec: any(sec.lower() in s.lower() for s in sections) + for sec in required_sections} + + return { + "title_page": title_checks, + "sections": section_checks + } diff --git a/main.py b/main.py index 5d2c207..9ad9184 100644 --- a/main.py +++ b/main.py @@ -10,6 +10,7 @@ from fastapi import UploadFile, File from dotenv import load_dotenv from itsdangerous import TimestampSigner, BadSignature +from grading import grade_report_latex import re load_dotenv() @@ -456,6 +457,45 @@ def grade_lab(course_id: str, group_id: str, lab_id: str, request: GradeRequest) lab_number = parse_lab_id(lab_id) row_idx = github_values.index(username) + 3 lab_col = student_col + lab_number + lab_offset + + report_checks = {} + if lab_config.get("report"): + report_file = "report.tex" # или можно брать из конфига + report_url = f"https://api.github.com/repos/{org}/{repo_name}/contents/{report_file}" + report_resp = requests.get(report_url, headers=headers) + + if report_resp.status_code == 200: + try: + # Получаем информацию о студенте из таблицы + student_name = sheet.cell(row_idx, student_col).value + report_checks = grade_report_latex( + repo=f"{org}/{repo_name}", + filename=report_file, + subject=course_info.get("name", ""), + # Учитывать, что все названия предметов на английском в конфигах + lab_name=lab_config.get("short-name", ""), + # Полного названия лабы в конфиге нет, поэтому пока берем short-name + group=group_id, + student_name=student_name, + required_sections=lab_config.get("report", []), + branch="main", + github_token=GITHUB_TOKEN + ) + + # Проверяем результаты + title_ok = all(report_checks["title_page"].values()) + sections_ok = all(report_checks["sections"].values()) + + if not title_ok or not sections_ok: + final_result = "✗" + summary.append("❌ Отчет не соответствует требованиям") + else: + summary.append("✅ Отчет соответствует требованиям") + + except Exception as e: + final_result = "✗" + summary.append(f"❌ Ошибка при проверке отчета: {str(e)}") + sheet.update_cell(row_idx, lab_col, final_result) return { From 6d3ec592257b8231eec34b6bca6a94c1756e4517 Mon Sep 17 00:00:00 2001 From: ev-osi Date: Wed, 23 Jul 2025 15:43:51 +0300 Subject: [PATCH 2/2] Upgrade grading.py --- grading.py | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/grading.py b/grading.py index 69852b5..59146c3 100644 --- a/grading.py +++ b/grading.py @@ -8,8 +8,8 @@ def extract_latex_content(text): # Удаление комментариев text = re.sub(r'%.*', '', text) - # Извлечение содержимого между \begin{document} и \end{document} - doc_match = re.search(r'\\begin{document}(.*?)\\end{document}', text, re.DOTALL) + # Извлечение содержимого между \begin{titlepage} и \end{document} + doc_match = re.search(r'\\begin{titlepage}(.*?)\\end{document}', text, re.DOTALL) if not doc_match: return "" content = doc_match.group(1) @@ -19,14 +19,6 @@ def extract_latex_content(text): lambda m: ' '.join(re.findall(r'\{([^{}]*)\}', m.group())), content, flags=re.DOTALL) - # Удаление LaTeX-команд с сохранением их аргументов - content = re.sub(r'\\[a-zA-Z]+\*?\s*\{([^{}]*)\}', r'\1', content) - content = re.sub(r'\\[a-zA-Z]+\*?\s*', ' ', content) - - # Удаление оставшихся спецсимволов - content = re.sub(r'[{}]', '', content) - content = re.sub(r'\\[^\s]*', '', content) - # Нормализация пробелов content = ' '.join(content.split()) return content @@ -42,7 +34,7 @@ def extract_titlepage_content(text): return "" content = titlepage_match.group(1) - # Обработка содержимого (аналогично extract_latex_content) + # Обработка содержимого content = re.sub(r'\\[a-zA-Z]+\*?\s*\{([^{}]*)\}', r'\1', content) content = re.sub(r'\\[a-zA-Z]+\*?\s*', ' ', content) content = re.sub(r'[{}]', '', content) @@ -101,7 +93,7 @@ def grade_report_latex(repo, filename, subject, lab_name, group, student_name, r } # Проверка разделов (ищем как \section{Name}, так и \section*{Name}) - sections = re.findall(r'\\section\*?\{([^{}]*)\}', content) + sections = re.findall(r'\\section\*?\{([^{}]*)\}', full_text) section_checks = {sec: any(sec.lower() in s.lower() for s in sections) for sec in required_sections} @@ -109,3 +101,4 @@ def grade_report_latex(repo, filename, subject, lab_name, group, student_name, r "title_page": title_checks, "sections": section_checks } +