"""
Утилиты для работы с домашними заданиями.
"""
import re
import os
import logging
from django.core.exceptions import ValidationError
logger = logging.getLogger(__name__)
# Теги, разрешённые после конвертации Markdown + MathML (для санитизации)
ALLOWED_FEEDBACK_HTML_TAGS = [
'p', 'br', 'div', 'span', 'strong', 'em', 'b', 'i', 'u', 's',
'ul', 'ol', 'li', 'code', 'pre', 'blockquote', 'h1', 'h2', 'h3', 'h4',
'a', 'sub', 'sup',
# MathML (для формул из LaTeX)
'math', 'mrow', 'mi', 'mo', 'mn', 'msup', 'msub', 'munder', 'mover',
'mfrac', 'mtable', 'mtr', 'mtd', 'mstyle', 'mtext', 'annotation', 'semantics',
]
def feedback_to_html(raw: str) -> str:
"""
Переводит ответ ИИ (markdown + LaTeX вроде $a$, $$x^2$$) в безопасный HTML.
Используется для отображения ai_feedback и комментариев проверки без «сырых» символов $.
"""
if not raw or not raw.strip():
return ''
try:
import markdown
import bleach
except ImportError:
logger.warning("markdown/bleach не установлены — возвращаем экранированный текст")
return _escape_html(raw)
# 1) Выносим LaTeX в placeholder'ы, чтобы markdown их не трогал
block_maths = []
inline_maths = []
def block_replacer(m):
block_maths.append(m.group(1).strip())
return f'\x00MATH_BLOCK_{len(block_maths) - 1}\x00'
def inline_replacer(m):
inline_maths.append(m.group(1).strip())
return f'\x00MATH_INLINE_{len(inline_maths) - 1}\x00'
text = raw
# Сначала блочные $$ ... $$
text = re.sub(r'\$\$([^$]*?)\$\$', block_replacer, text, flags=re.DOTALL)
# Потом инлайновые $ ... $ (не захватываем переносы в инлайне)
text = re.sub(r'\$([^$\n]+?)\$', inline_replacer, text)
# 2) Markdown → HTML
html = markdown.markdown(
text,
extensions=['nl2br'],
output_format='html5',
)
# 3) LaTeX → MathML и подставляем обратно
try:
from latex2mathml.converter import convert as latex_to_mathml
except ImportError:
latex_to_mathml = None
def replace_math(placeholder_prefix, maths_list):
for i, latex in enumerate(maths_list):
token = f'\x00{placeholder_prefix}_{i}\x00'
if latex_to_mathml and latex:
try:
mathml = latex_to_mathml(latex)
# latex2mathml возвращает полный
html_replacement = f'{mathml}'
except Exception as e:
logger.debug("LaTeX → MathML failed for %r: %s", latex[:50], e)
html_replacement = _escape_html(f'${latex}$')
else:
html_replacement = _escape_html(f'${latex}$')
nonlocal html
html = html.replace(token, html_replacement)
replace_math('MATH_BLOCK', block_maths)
replace_math('MATH_INLINE', inline_maths)
# 4) Санитизация (разрешаем class, xmlns для формул и ссылок href)
allowed_attrs = {'*': ['class'], 'a': ['href', 'title', 'rel'], 'math': ['xmlns'], 'span': ['class']}
html = bleach.clean(html, tags=ALLOWED_FEEDBACK_HTML_TAGS, attributes=allowed_attrs, strip=True)
return html.strip()
def _escape_html(s: str) -> str:
"""Экранирует HTML-сущности."""
return (
s.replace('&', '&')
.replace('<', '<')
.replace('>', '>')
.replace('"', '"')
)
def sanitize_filename(filename):
"""
Очистка имени файла от опасных символов.
Args:
filename: Исходное имя файла
Returns:
str: Безопасное имя файла
"""
# Удаляем путь, оставляем только имя файла
filename = os.path.basename(filename)
# Удаляем опасные символы
filename = re.sub(r'[<>:"/\\|?*\x00-\x1f]', '', filename)
# Удаляем ведущие/завершающие точки и пробелы
filename = filename.strip('. ')
# Ограничиваем длину
if len(filename) > 255:
name, ext = os.path.splitext(filename)
filename = name[:250] + ext
# Если имя пустое, возвращаем дефолтное
if not filename:
filename = 'file'
return filename
def validate_file_type(filename, allowed_types):
"""
Проверка типа файла.
Args:
filename: Имя файла
allowed_types: Строка с разрешенными расширениями (например, ".pdf,.doc,.docx")
Returns:
bool: True если тип разрешен
"""
if not allowed_types:
return True
# Получаем расширение файла
ext = os.path.splitext(filename)[1].lower()
# Нормализуем allowed_types
allowed_list = [t.strip().lower() for t in allowed_types.split(',') if t.strip()]
return ext in allowed_list
def validate_file_size(file_size, max_size):
"""
Проверка размера файла.
Args:
file_size: Размер файла в байтах
max_size: Максимальный размер в байтах
Returns:
bool: True если размер допустим
"""
if max_size <= 0:
return True
return file_size <= max_size