Данный проект представляет из себя систему для автоматического тестирования задач, написанных на языке Python.
Тетирующая система - платформа для проведения контестов по программированию. Есть задачи, есть турниры, есть тесты и есть код, это всё надо связать и по итогам выстраивать рейтинг решений.
- сделать минимальный функционал: связать контесты, задачи, пользователей
- сделать автоматическое тестирование задач (как минимум на Python)
- добавить в регистрацию подтверждение почты
- дать возможность пользователям создавать свои задачи/турниры
- защитить сервер от 'атак' в коде (как выполнение 'DROP TABLE' или 'os.system("shutdown")')
- создать api для работы с тестирующей системой
- сделать тестирование асинхронное, чтобы не подвисал сайт
У нас есть несколько основных пунктов: пользователи, контесты, задачи, тесты для задач, попытки пользователей. Контесты к тому же связаны с задачами, задачи с тестами, попытки с задачами и т.д. Схема того, как в приложении выглядит бд:
- database.db
- attempts (основная информация о попытках по задачам)
- id
- contest_id
- task_id
- user_id
- solution (исходный код)
- time (время отправки)
- score (результат попытки)
- status (статус попытки)
- contests (информация о контестах)
- id
- creators (строчка с id пользователей, которые имеют доступ редактировать контест)
- title (название контеста)
- description (описание контеста)
- tasks (строчка с id задач, которые присутствуют в контесте, порядок имеет значение)
- start_time (время начала контеста)
- end_time (время конца контеста)
- tasks (информация о задачах)
- id
- creator (id создателя, создатель всегда один)
- time_limit (ограничение по времени)
- title (название задачи)
- description (описание задачи)
- reference (код, который проходит все тесты)
- attempts (колличество попыток по задаче)
- successful (колличесто удачных попыток по задаче)
- tests (информация о тестах для задач)
- id
- task_id
- input (входные данные)
- output (выходные данные, генерируются tasks.reference запущенным на input)
- output (выходные данные)
- points (сколько баллоы приносит данный тест)
- trial (информация о каждом тесте, как код пользователя себя повёл)
- attempt_id
- test_id
- status (статус как код себя повёл на тесте)
- output (информация что вывел код, заполняется когда status=WA)
- users (информация о пользователях)
- id
- username (ник пользователя, уникальное значение)
- email (почта, уникальное значение)
- password (хэшированный с солью пароль)
- name (имя)
- surname (фамилия)
- register_date (дата регистрации)
- city (город)
- creator (булевое поле, 0 - не имеет доступ создавать задачи, 1 - имеет доступ)
- verification (поле для разных ключей: подтверждение почты/получени ключа api)
- registered (булевое поле, 0 - не зарегестрирован, 1 - зарегестрирован)
- api_key (ключ для работы с api)
- attempts (основная информация о попытках по задачам)
Работа была разбита на 5 блоков:
- написание дизайна и связка с бд
- написание функционала ресурсов в бд/структурирование бд
- написание почты
- написание самой тестирующей системы и выставления вердиктов
- написание api
Функционал тестирующей системы таков:
- авторизация с подтверждением почты
- просмотр всех задач
- просмотр всех контестов
- создание/редактирование/удаление контестов
- создание/редактирование/удаление задач
- редактирование/удаление пользователя
- запрос на получение прав создания задач
- автоматическое тестирование задач
- просмотр контестов/задач/пользователя через api
pip install -r requirements.txt
- Открываем
config.json
и заполняем там поля:SALT
- соль для хэша, любая случайная длинная строкаAPP_KEY
- ключ приложения, также любая случайная длинная строкаPYTHON_INTERPRETER
- краткое название интерпретатора питона (python/python3/python3.7)MAIL_PASSWORD_GOOGLE
- пароль от гугл аккаунта, с которого будут отправлены письмаMAIL_LOGIN_GOOGLE
- логин от гугл аккаунта, с которого будут отправлены письмаHOST
- где будет запускаться ваш кодHOST_FULLNAME
- полное имя сайта (с http) где находится сервер, туда будет перенаправлять почта
python main.py
Для загрузки тестов, вы должны прикладывать .json файл в следующем формате:
[
{
"input": "3\n5"
},
{
"input": "5\n10"
},
{
"input": "573\n923",
"points": 3
}
]
Интерпритатор питона обязательно должен быть версии 3.7+, так как версия subprocess на питонах ниже не имела нужный функционал
Если у вас MacOS, то вероятно вам прийдётся в PYTHON_INTERPRETER
указать python3.7
Гугл почта обязательна, никакой Яндекс/mail не будут работать
Вы можете не задавать данные в config.json
, вы можете создать переменные среды
В коде асинхронно запускается отправка GET запросов на себя же, это было сделано чтобы сайт не уходил в состояние idling при деплое на heroku
Страницы не работают на API т.к. у API уменьшенный функционал с целью безопасности
import create_environment
- задаёт переменные среды, эту строчку не стирать/менять место
Чтобы можно было решать все задачи, то создаётся специальный нулевой контест, куда добавляется каждая задача (models.py -> __ init __())
if Contest.get_contest(contest_id=0) is None:
session.add(Contest(id=0, creators='', title='Tasks', description='Contest with all tasks', start_time=0, end_time=pow(2, 34), tasks=''))
session.commit()
В переменной email_types
узакывается 'имя шаблона', 'имя шаблона'_plain, 'имя шаблона'_subject,
что позваляет отправять шаблоны сообщений легко и просто (email_sender.py)
def send_email(receiver: str, content_type: str, **kwargs) -> bool:
Процесс запроса максимально упрощён до уровня 'напишите почему'.
Письма с запросами отправляются на адрес администратора MAIL_LOGIN_GOOGLE
(models.py -> User -> send_creator_email())
send_email(environ['MAIL_LOGIN_GOOGLE'], content_type='get_creator', message=message, user_id=self.id, confirmation_key=self.verification)
При создании задачи код, вы не указываете в тестах output
с целью того, чтобы
в базу дыннх верно записались выходные данные. И это позваляет также убрать все тесты,
где код эталон не проходит по времени (models.py -> Task -> add_task())
for test in (tests if tests is not None else []):
tests_statuses.append(Test.add_test(task_id=task_id, inp=test.get('input'), points=test.get('points')))