uchill/backend/apps/homework/utils.py

172 lines
5.8 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.

"""
Утилиты для работы с домашними заданиями.
"""
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 возвращает полный <math>...</math>
html_replacement = f'<span class="math-wrap">{mathml}</span>'
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('&', '&amp;')
.replace('<', '&lt;')
.replace('>', '&gt;')
.replace('"', '&quot;')
)
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