Merge branch 'master' of gitlab.prodcontest.ru:team-15/project

This commit is contained in:
moolcoov
2025-03-03 20:36:12 +03:00
17 changed files with 682 additions and 130 deletions
+32
View File
@@ -1 +1,33 @@
# DataRush # DataRush
Инновационный сервис для проведения соревнований по анализу данных
## Запуск
Склонируйте репозиторий и пропишите
```bash
docker compose up
```
## Основные ручки
* `/` - основное приложение
* `/api/v1/docs` - swagger-ui документация
* `/admin` - админка
* `/admin/grafana` - графана
* `/docs` - гайд по анализу данных
После запуска по методу выше создается пользователь в админке (`/admin`) с данными ниже:`admin`
- `admin` - логин
- `proooooood` - пароль
## Тесты
Написаны unit-тесты (на базе Django TestCase) и E2E (Postman коллекция)
![Postman data](img/postman.gif)
![django test]()
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 261 KiB

+1
View File
@@ -67,3 +67,4 @@ class TaskAttachmentSchema(ModelSchema):
class TaskStatusSchema(Schema): class TaskStatusSchema(Schema):
task_name: str task_name: str
result: int result: int
max_points: int
+10 -4
View File
@@ -120,7 +120,6 @@ def submit_task(
user_input = content.read() user_input = content.read()
correct_answer = task.correct_answer_file.read() correct_answer = task.correct_answer_file.read()
verdict = user_input == correct_answer verdict = user_input == correct_answer
print(user_input, correct_answer)
submission = CompetitionTaskSubmission.objects.create( submission = CompetitionTaskSubmission.objects.create(
user=user, user=user,
task=task, task=task,
@@ -196,14 +195,21 @@ def get_competition_results(request, competition_id: UUID):
for task in tasks: for task in tasks:
submissions = CompetitionTaskSubmission.objects.filter( submissions = CompetitionTaskSubmission.objects.filter(
user=request.auth, task=task user=request.auth, task=task
).filter(status="checked").all() ).filter(status="checked").order_by("-earned_points").all()
if not submissions: if not submissions:
result = 0 all_submissions_count = CompetitionTaskSubmission.objects.filter(
user=request.auth, task=task
).count()
if all_submissions_count == 0:
result = -2
else:
result = -1
else: else:
result = submissions[0].earned_points result = submissions[0].earned_points
data.append(TaskStatusSchema( data.append(TaskStatusSchema(
task_name=task.title, task_name=task.title,
result=result result=result,
max_points=task.points,
)) ))
return status.OK, data return status.OK, data
Binary file not shown.

After

Width:  |  Height:  |  Size: 116 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

@@ -2,6 +2,7 @@ import random
import uuid import uuid
from datetime import timedelta, datetime from datetime import timedelta, datetime
from PIL.Image import Image
from django.conf import settings from django.conf import settings
from django.contrib.auth.hashers import make_password from django.contrib.auth.hashers import make_password
from django.core.files.base import ContentFile, File from django.core.files.base import ContentFile, File
@@ -13,10 +14,11 @@ from apps.review.models import Reviewer
from apps.task.models import ( from apps.task.models import (
CompetitionTask, CompetitionTask,
CompetitionTaskCriteria, CompetitionTaskCriteria,
CompetitionTaskSubmission, CompetitionTaskSubmission, CompetitionTaskAttachment,
) )
from apps.user.models import User, UserRole from apps.user.models import User, UserRole
# Примеры файлов с правильными ответами
ans1 = ContentFile( ans1 = ContentFile(
b"1984", b"1984",
name=f"submission_{uuid.uuid4().hex}.txt", name=f"submission_{uuid.uuid4().hex}.txt",
@@ -25,46 +27,86 @@ ans2 = ContentFile(
b"3", b"3",
name=f"submission_{uuid.uuid4().hex}.txt", name=f"submission_{uuid.uuid4().hex}.txt",
) )
ans3 = ContentFile(
b"42",
name=f"submission_{uuid.uuid4().hex}.txt",
)
ans4 = ContentFile(
b"11",
name=f"submission_{uuid.uuid4().hex}.txt",
)
dataset = ContentFile(
b"it is a dataset",
name=f"dataset-{uuid.uuid4().hex}.txt",
)
dataset2 = ContentFile(
b"it is a dataset",
name=f"dataset-{uuid.uuid4().hex}.csv",
)
now = timezone.now() now = timezone.now()
image_dir = f"{settings.BASE_DIR}/apps/core/contents/images"
f = open(f"{image_dir}/dano.jpg", "rb")
dano_image = File(f, name="dano.jpg")
# Расширенный список соревнований, включая 3 новых
competitions = [ competitions = [
{ {
"obj": None, # докидывает в процессе "obj": None, # будет заполнено позже
"title": "DANO. Финал", "title": "DANO. Финал",
"description": "Олимпиада по анализу данных от Т-Банка и ВШЭ", "description": "Олимпиада по анализу данных от Т-Банка и ВШЭ",
"start_date": now - timedelta(days=2), "start_date": now - timedelta(days=2),
"end_date": now + timedelta(days=5), "end_date": now + timedelta(days=5),
"type": "competitive", "type": "competitive",
"participation_type": "solo", "participation_type": "solo",
"image": dano_image,
"tasks": [ "tasks": [
{ {
"obj": None, "obj": None,
"title": "Задача 1", "title": "Задача 1",
"description": """На маркетплейсе «Е-шопинг» продаются различные товары. Одна из задач аналитика — "description": (
прогнозировать, сколько товаров будет продаваться при определенной цене. В ходе """
исследований и экспериментов был выявлен вид зависимости: На маркетплейсе «Е-шопинг» продаются различные товары. Одна из задач аналитика —
$Q(P) = Q_0 \times e^{E \times \frac{P_0 - P}{P_0}}$ прогнозировать, сколько товаров будет продаваться при определенной цене. В ходе исследований
был выявлен вид зависимости:\n
$Q(P) = Q_0 \\times e^{E \\times \\frac{P_0 - P}{P_0}}$\n
где Q — это количество проданных единиц товара при цене P, где Q — это количество проданных единиц товара при цене P,
Q 0 — количество проданных единиц товара при цене P0 , Q 0 — количество проданных единиц товара при цене P0 ,
E — коэффициент чувствительности количества проданных единиц товара к изменению E — коэффициент чувствительности количества проданных единиц товара к изменению
цены. цены.
1. Кофемашину «Кофе каждый день» купили 200 000 раз (Q 0 ) при цене 20 000 ₽ (P 0
).
Позже продавец поднял цену на 5 000 ₽, при этом продажи сократились на 24 000
штук. Какой коэффициент чувствительности Е имеет этот товар? Ответ округлите
до двух знаков после запятой.
2. Потом продавец решил поставить новую цену на эту же модель: 22 000 ₽. Сколько
продаж согласно нашей зависимости будет у этого товара? Используйте результаты
предыдущего пункта. Ответ округлите до целых.
3. Другой продавец предлагает на нашем маркетплейсе кухонные ножи и сковородки.
Благодаря исследованиям были получены следующие формулы зависимостей
количества проданных товаров:
Найдите, сколько заработает продавец при цене по 3 000 ₽ за нож и сковороду Найдите, сколько заработает продавец при цене по 3 000 ₽ за нож и сковороду
при условии, что себестоимость ножа — 1 000 ₽, а сковородки — 2 000 ₽.Ответ при условии, что себестоимость ножа — 1 000 ₽, а сковородки — 2 000 ₽.Ответ
округлите до целых.""".strip(), округлите до целых.
Найдите, сколько заработает продавец при цене по 3 000 ₽ за нож и сковороду при условии,
что себестоимость ножа — 1 000 ₽, а сковородки — 2 000 ₽. Ответ округлите до целых.
""".strip()
),
"type": CompetitionTask.CompetitionTaskType.INPUT.value, "type": CompetitionTask.CompetitionTaskType.INPUT.value,
"points": 3, "points": 3,
"submission_reviewers_count": 2, "submission_reviewers_count": 2,
"max_attempts": 20, "max_attempts": 20,
"correct_answer_file": ans1 "correct_answer_file": ans1,
}, },
{ {
"obj": None, "obj": None,
"title": "Задача 2", "title": "Задача 2",
"description": """ "description": "Найдите максимальную зарплату программиста из датасета на питоне",
Напишите "hello_dano" на питоне
""".strip(),
"type": CompetitionTask.CompetitionTaskType.CHECKER.value, "type": CompetitionTask.CompetitionTaskType.CHECKER.value,
"attachment": dataset,
"attachment_path": "dataset",
"points": 25, "points": 25,
"submission_reviewers_count": 2, "submission_reviewers_count": 2,
"max_attempts": 50, "max_attempts": 50,
@@ -72,7 +114,10 @@ E — коэффициент чувствительности количеств
{ {
"obj": None, "obj": None,
"title": "Задача 3", "title": "Задача 3",
"description": """Небольшой интернет-магазин собрал данные о действиях пользователей на своем сайте "attachment": dataset2,
"attachment_path": "dataset2",
"description": """
Небольшой интернет-магазин собрал данные о действиях пользователей на своем сайте
за последние несколько месяцев. за последние несколько месяцев.
ecommerce_logs.csv — журнал действий пользователей: ecommerce_logs.csv — журнал действий пользователей:
• user_id — идентификатор пользователя. • user_id — идентификатор пользователя.
@@ -89,19 +134,22 @@ ecommerce_logs.csv — журнал действий пользователей:
• sex — пол пользователя. • sex — пол пользователя.
• region — регион пользователя. • region — регион пользователя.
• price — цена товара. • price — цена товара.
Ваша задача — проанализировать поведение пользователей, выявить возможные
проблемы при покупке и предложить решения. Ваш анализ поможет понять, на каком
этапе воронки магазин теряет покупателей и какие изменения можно внести, чтобы
улучшить процесс покупок в интернет-магазине.
Как правило, количество пользователей на каждом последующем шаге уменьшается,
и такая ситуация называется “воронкой”. Конверсия — это отношение количества
пользователей на каком-то одном шаге к количеству пользователей на одном
из предыдущих шагов. Например, конверсия из визита сайта в добавление товара
в корзину рассчитывается так: количество пользователей, добавивших товар в корзину,
делится на количество пользователей, посетивших сайт.
Вам нужно изучить воронку конверсии, которая показывает, как пользователи переходят Вам нужно изучить воронку конверсии, которая показывает, как пользователи переходят
от одного шага к другому на сайте. В нашем случае воронка состоит из следующих шагов: от одного шага к другому на сайте. В нашем случае воронка состоит из следующих шагов:
1. Посещение сайта. 1. Посещение сайта.
2. Просмотр карточки товара. 2. Просмотр карточки товара.
3. Добавление товара в корзину. 3. Добавление товара в корзину.
4. Покупка. 4. Покупка.
1. Посещение сайта.
2. Просмотр карточки товара.
3. Добавление товара в корзину.
4. Покупка.
3 / 11
1.) Посчитайте конверсию (округлите ответ до 3 знаков после запятой): 1.) Посчитайте конверсию (округлите ответ до 3 знаков после запятой):
• Из визита на сайт в клик на карточку товара. • Из визита на сайт в клик на карточку товара.
• Из клика в добавление в корзину. • Из клика в добавление в корзину.
@@ -136,18 +184,18 @@ ecommerce_logs.csv — журнал действий пользователей:
"name": "Обоснованность решения", "name": "Обоснованность решения",
"slug": "validity", "slug": "validity",
"description": "Аргументация", "description": "Аргументация",
"max_value": 5 "max_value": 5,
}, },
{ {
"obj": None, "obj": None,
"name": "Правильность", "name": "Правильность",
"slug": "correctness", "slug": "correctness",
"description": "Насколько точные и верные ответы были представлены.", "description": "Точность вычислений",
"max_value": 5 "max_value": 5,
}
]
}, },
] ],
},
],
}, },
{ {
"obj": None, "obj": None,
@@ -161,53 +209,225 @@ ecommerce_logs.csv — журнал действий пользователей:
{ {
"obj": None, "obj": None,
"title": "Задача 1", "title": "Задача 1",
"description": """Сколько этапов в DANO?""".strip(), "description": """
Конверсия — это доля клиентов, перешедших с одного этапа на другой. Например,
на сайт с заявками на кредитные карты зашли 50 человек, после ознакомления
с условиями заявку на оформление карты (далее — заявку) оставили только 45
из них. В данном случае конверсия составляет 90% = 45/50.
Рассмотрим следующую ситуацию. В ноябре сайт посетили 100 мужчин и 100
женщин, при этом из них заявки оставили 10 мужчин и всего 5 женщин.
1. Посчитайте конверсии для мужчин и для женщин из захода на сайт
в оформление заявки.
2. Посчитайте общую конверсию для всех пользователей.
3. В декабре была проведена дополнительная рекламная компания, и общее
число зашедших на сайт стало больше. При этом конверсия для мужчин стала
равна 12%, а для женщин — 7%. Может ли быть такое, что общая конверсия
в декабре упала? Если да, то приведите численный пример. Если нет —
докажите.
4. При условии увеличения конверсий у мужчин и у женщин до 12% и 7%
соответственно в каком интервале будет лежать общая конверсия? Обоснуйте
свой ответ.
""".strip(),
"type": CompetitionTask.CompetitionTaskType.INPUT.value, "type": CompetitionTask.CompetitionTaskType.INPUT.value,
"points": 3, "points": 3,
"submission_reviewers_count": 2, "submission_reviewers_count": 2,
"max_attempts": 20, "correct_answer_file": ans2,
"correct_answer_file": ans2
}, },
{ {
"obj": None, "obj": None,
"title": "Задача 2", "title": "Задача 2",
"description": """ "description": """
Напишите отзыв про DANO(Хороший) Каждый день Дима звонит в пекарню, чтобы узнать, есть ли сегодня в продаже его
любимые булочки с повидлом. За последние 3 дня булочки были в наличии 2 раза,
а 1 раз их не было.
Пусть переменная Х = 0, если булочек нет, и Х = 1, если булочки есть. Наличие
булочек в конкретный день не зависит от наличия булочек в любой другой день.
1. Сколько наблюдений собрал Дима? Выпишите все значения из его выборки
через запятую. Посчитайте для этой выборки среднее значение Х, дисперсию Х.
В какой доле случаев булочки были в наличии?
2. Пусть p — вероятность того, что булочки в наличии (p не может быть меньше 0
и больше 1. Например, p может быть равно 0,2 = 1/5 — то есть в одном из пяти
случаев булочки в наличии). Чему равна вероятность, что сегодня булочки
есть, а завтра их не будет? Чему равна вероятность, что за два дня в один день
булочки будут, а в другой — не будут? (Напишите два выражения, зависящие
от p.)
3. Чему равна вероятность получения наблюдений как у Димы? (Напишите одно
выражение, зависящее от p.)
4. При каком значении p вероятность получить выборку как у Димы максимальна?
Вычислите его. Как это значение соотносится с наблюдениями Димы?
5. Дима нашел значение p из предыдущего пункта и сделал вывод, что
на следующий день булочки испекут с вероятностью p. Верный ли вывод сделал
Дима? Поясните свой ответ.
6. Рядом с домом Димы открыли новую пекарню, где тоже делают булочки
с повидлом. Дима решил сравнить две пекарни. Для этого он собрал выборку
за 100 дней: в новой пекарне булочки были в наличии 70 дней, в старой — 60
дней. Какую гипотезу может проверить Дима? Какой механизм, может лежать
в основе этой гипотезы? При описании механизма вы можете сами дополнить
историю Димы (например, предположить расположение старой пекарни,
себестоимость повидла и другое, что могло бы помочь объяснить гипотезу,
необязательно рассматривать приведенные примеры). Помогите Диме
проверить описанную вами гипотезу. Есть ли разница в производительности
между новой пекарней и старой?
""".strip(), """.strip(),
"type": CompetitionTask.CompetitionTaskType.REVIEW.value, "type": CompetitionTask.CompetitionTaskType.REVIEW.value,
"points": 15, "points": 15,
"submission_reviewers_count": 2, "submission_reviewers_count": 2,
"max_attempts": 1,
"criteries": [ "criteries": [
{ {
"obj": None, "obj": None,
"name": "Хорошесть отзыва", "name": "Хорошесть отзыва",
"slug": "validity", "slug": "validity",
"description": "Хорошесть", "description": "Критерий качества отзыва",
"max_value": 10 "max_value": 10,
}, },
{ {
"obj": None, "obj": None,
"name": "Подробность", "name": "Подробность",
"slug": "detail", "slug": "detail",
"description": "Насколько подробно расписан ответ.", "description": "Насколько подробно расписан ответ",
"max_value": 5 "max_value": 5,
} },
] ],
}, },
{ {
"obj": None, "obj": None,
"title": "Задача 3", "title": "Задача 3",
"description": """ "description": """
Напишите выведите 1+3 на питоне Вы аналитик ведущей игровой компании GameMasters Inc., которая
специализируется на разработке мобильных игр. Ваши коллеги разработали
обновленный игровой магазин, в котором игроки могут приобретать внутриигровые
предметы и суперспособности. Ваша задача — провести сравнение, чтобы
определить, как внедрение нового магазина повлияло на поведение пользователей
в игре.
Для этого пользователи были разделены на две равные группы случайным образом:
А — пользователи, которым доступен только старый магазин;
B — пользователи, которым доступен только обновленный магазин.
Спустя месяц после запуска по каждому пользователю из каждой группы были
посчитаны следующие метрики:
revenue_per_user — доход, который был получен от пользователя за период;
orders_cnt_per_user — количество заказов, которое совершено пользователем
за период;
converted_from_main_screen_to_item_card_screen — флаг захода на экран
с товарами (0 — если пользователь не заходил на карточку товара, 1 — если
заходил).
В таблице приведены значения этих метрик. Также в ней находится столбец ‘group’,
в котором указано, к какой группе (A или B) относится пользователь и столбец
‘period’ — характеризующий значение метрик до начала теста и во время проведения
теста. На карточке товара содержится дополнительная информация, фотография
и его характеристики. Однако оплатить товар можно и без захода на карточку
товара.
Задача: сравните группы по каждой метрике и сделайте вывод о том, стоит ли
продолжить внедрение обновленного магазина или нужно вернуть старый
""".strip(), """.strip(),
"type": CompetitionTask.CompetitionTaskType.CHECKER.value, "type": CompetitionTask.CompetitionTaskType.CHECKER.value,
"points": 30, "points": 30,
"submission_reviewers_count": 2, "submission_reviewers_count": 2,
"max_attempts": 100, },
} ],
] },
} {
"obj": None,
"title": "Data Challenge 2025(FAKE DANO)",
"description": """
Ну типо дано
""".strip(),
"start_date": now - timedelta(days=1),
"end_date": now + timedelta(days=10),
"type": "competitive",
"participation_type": "solo",
"tasks": [
{
"obj": None,
"title": "Анализ трендов", # TODO сюда добавить бд
"description": """
Скачайте базу данных со специальной страницы (https://dano.hse.ru/data), изучите ее более
внимательно: посмотрите на переменные, посчитайте описательные статистики, постройте
предварительные графики и таблицы, обратите внимание на выбросы.
Продумайте все детали: исследовательский вопрос, гипотезу, механизм и пр.
Разработайте дизайн исследования:
• что вам нужно для того, чтобы ответить на исследовательский вопрос
• что нужно чтобы проверить ту или иную гипотезу
• какие таблицы и графики вам понадобятся
• какую информацию из них можно извлечь
• как интерпретировать получаемые результаты
• помогает ли это в вашем исследовании
• несет ли полезную информацию
• действительно ли эти построенные таблицы и графики необходимы и продвигают ваш
проект или может быть необходимы другие
• какие методы и модели вам нужны
• в какой последовательности выполнять все расчеты и построения
Обращайте особое внимание на то, что все эти процедуры должны быть оправданы и
продвигать вас в направлении поиска ответа на исследовательский вопрос.
Распределите задачи между членами команды. Установите сроки. Придерживайтесь взятых
на себя обязательств и данных друг другу обещаний – делайте все в срок. Обсуждайте между
собой полученные результаты, ищите наиболее удачный способ проверить ваши гипотезы,
наиболее удачные графики и таблицы. Советуйтесь с ментором, обращайтесь к нему за
помощью – его задача помочь вам отобрать правильные идеи и подсказать как их технически
реализовать.
Заведите общее облачное пространство, где будут хранится все ваши результаты.
Структурируйте, создавайте необходимые папки, называйте папки и документы говорящими
именами, оставляйте комментарии
""".strip(),
"type": CompetitionTask.CompetitionTaskType.REVIEW.value,
"points": 20,
"submission_reviewers_count": 3,
"max_attempts": 2,
"attachment": dataset,
"attachment_path": "dataset",
"criteries": [
{
"obj": None,
"name": "Качество анализа",
"slug": "analysis_quality",
"description": "Глубина анализа данных",
"max_value": 10,
},
{
"obj": None,
"name": "Обоснованность выводов",
"slug": "insight",
"description": "Логичность выводов",
"max_value": 10,
},
],
},
{
"obj": None,
"title": "Ещё задачка",
"description": """
Как известно, Израиль является одной из лидирующих стран по темпам вакцинации. По
данным на июнь 2021 г. в стране вакцинировано 60% граждан (85% взрослого населения).
Однако среди заразившихся в этом же месяце (июне 2021 года), как признали власти
Израиля, примерно половина была уже вакцинирована. Что можно сказать об
эффективности вакцины на основании этих данных?
1) Данные не свидетельствуют об эффективности вакцины, т. к. вероятность
заразиться составляет 50%, независимо от того, вакцинировался человек или нет
2) Данные не свидетельствуют об эффективности вакцины, т. к. среди
вакцинированных есть заразившиеся
3) Данные свидетельствуют об эффективности вакцины, т. к. если бы она не работала,
доля вакцинированных среди заболевших была бы равна доле вакцинированных
среди всего населения страны
4) Данные свидетельствуют об эффективности вакцины, т. к. вакцинированные
переносят болезнь в более легкой форме
""".strip(),
"type": CompetitionTask.CompetitionTaskType.INPUT.value,
"points": 15,
"submission_reviewers_count": 2,
"max_attempts": 50,
"correct_answer_file": ans3
},
{
"obj": None,
"title": "Быстрый ответ",
"description": "Сколько будет 6 * 7?",
"type": CompetitionTask.CompetitionTaskType.INPUT.value,
"points": 5,
"submission_reviewers_count": 2,
"max_attempts": 10,
"correct_answer_file": ans3,
},
],
},
] ]
users = [ users = [
@@ -222,20 +442,163 @@ users = [
"username": "dreamonovich", "username": "dreamonovich",
"password": "password123!", "password": "password123!",
"role": UserRole.STUDENT.value, "role": UserRole.STUDENT.value,
} },
{
"email": "alisa.kuznetsova@gmail.com",
"username": "alisa_kuz",
"password": "password123!",
"role": UserRole.STUDENT.value,
},
{
"email": "ivan.petrov@gmail.com",
"username": "ivan_petrov",
"password": "password123!",
"role": UserRole.STUDENT.value,
},
{
"email": "olga.sidorova@gmail.com",
"username": "olga_sid",
"password": "password123!",
"role": UserRole.STUDENT.value,
},
{
"email": "karim@gmail.com",
"username": "karim",
"password": "password123!",
"role": UserRole.STUDENT.value,
},
{
"email": "noble@gmail.com",
"username": "noble",
"password": "password123!",
"role": UserRole.STUDENT.value,
},
{
"email": "koller@gmail.com",
"username": "koller",
"password": "password123!",
"role": UserRole.STUDENT.value,
},
{
"email": "gold_checker@gmail.com",
"username": "gold_checker",
"password": "password123!",
"role": UserRole.STUDENT.value,
},
{
"email": "looka@gmail.com",
"username": "looka",
"password": "password123!",
"role": UserRole.STUDENT.value,
},
{
"email": "danil_malikov@gmail.com",
"username": "danil_malikov",
"password": "password123!",
"role": UserRole.STUDENT.value,
},
{
"email": "marina-looks@gmail.com",
"username": "marina-looks",
"password": "password123!",
"role": UserRole.STUDENT.value,
},
{
"email": "pasha@gmail.com",
"username": "pasha",
"password": "password123!",
"role": UserRole.STUDENT.value,
},
{
"email": "oleg-tinkov@gmail.com",
"username": "oleg-tinkov",
"password": "password123!",
"role": UserRole.STUDENT.value,
},
{
"email": "baron_ivanych@gmail.com",
"username": "baron_ivanych",
"password": "password123!",
"role": UserRole.STUDENT.value,
},
] ]
reviewers = [ reviewers = [
{ {
"name": "Владислав", "name": "Владислав",
"surname": "Пикиневич", "surname": "Пикиневич",
"token": "aa443163-9861-4b8d-b8f7-81ecd25f6088" "token": "aa443163-9861-4b8d-b8f7-81ecd25f6088",
}, },
{ {
"name": "Александр", "name": "Александр",
"surname": "Шахов", "surname": "Шахов",
"token": "d2e8904a-01dd-4f84-a8b0-8a60f1a3b6c0" "token": "d2e8904a-01dd-4f84-a8b0-8a60f1a3b6c0",
} },
{
"name": "Мария",
"surname": "Иванова",
"token": "e3f8904a-23cd-4f84-a8b0-9b70f1a4b7d1",
},
{
"name": "Сергей",
"surname": "Смирнов",
"token": "f4g9015b-45de-5g95-b9c1-0c81g2b5c8e2",
},
{
"name": "Паша",
"surname": "Проверкин",
"token": "f4g9015b-45de-5g95-b9c1-0c81g2b3c8e2",
},
{
"name": "Илья",
"surname": "Продкин",
"token": "f4g9015b-45de-5g95-b8c1-0c81g2b5c8e2",
},
{
"name": "Влад",
"surname": "Проверкин",
"token": "f4g9015b-45de-5g95-b9c1-0c81g2b5c8e1",
},
{
"name": "Сашка",
"surname": "Пашкин",
"token": "f4g9015b-45de-5g95-b9c1-1c81g2b5c8e2",
},
{
"name": "Чарльз",
"surname": "Проверкин",
"token": "b4g9015b-45de-5g95-b9g1-0c81g2b5c8e2",
},
{
"name": "Тимурка",
"surname": "Проверкин",
"token": "f4g9015b-25de-5g95-b9c1-0c81g2b5c8e2",
},
{
"name": "Александр",
"surname": "Даношкин",
"token": "f4g9015t-45de-5g95-b9c1-0c81g2b5c8e2",
},
{
"name": "Паша",
"surname": "Проверкин",
"token": "f4g9015r-45de-5g95-b9c1-0c81g2b5c8e2",
},
{
"name": "Лука",
"surname": "Проверкин",
"token": "f4g9015e-45de-5g95-b9c1-0c81g2b5c8e2",
},
{
"name": "Кирилл",
"surname": "Проверкин",
"token": "f4g9015w-45de-5g95-b9c1-0c81g2b5c8e2",
},
{
"name": "Олег",
"surname": "Проверкин",
"token": "f4g9015q-45de-5g95-b9c1-0c81g2b5c8e2",
},
] ]
class Command(BaseCommand): class Command(BaseCommand):
@@ -251,6 +614,8 @@ class Command(BaseCommand):
self.create_states(competitions, users) self.create_states(competitions, users)
self.stdout.write("Data generation completed.") self.stdout.write("Data generation completed.")
f.close()
def create_reviewers(self, count): def create_reviewers(self, count):
reviewers_objs = [] reviewers_objs = []
for reviewer in reviewers: for reviewer in reviewers:
@@ -290,6 +655,10 @@ class Command(BaseCommand):
participation_type=competition['participation_type'], participation_type=competition['participation_type'],
) )
if competition.get("image"):
competition_obj.image_url = competition['image']
competition_obj.save()
competitions[i]['obj'] = competition_obj competitions[i]['obj'] = competition_obj
competition_obj.participants.add(*users) competition_obj.participants.add(*users)
competitions_objs.append(competition_obj) competitions_objs.append(competition_obj)
@@ -306,19 +675,26 @@ class Command(BaseCommand):
for i, competition in enumerate(competitions): for i, competition in enumerate(competitions):
for j, task in enumerate(competition['tasks']): for j, task in enumerate(competition['tasks']):
task_obj = CompetitionTask.objects.create( task_obj = CompetitionTask.objects.create(
in_competition_position=j, in_competition_position=j+1,
competition=competition['obj'], competition=competition['obj'],
title=task['title'], title=task['title'],
description=task['description'], description=task['description'],
type=task['type'], type=task['type'],
points=task['points'], points=task['points'],
submission_reviewers_count=task['submission_reviewers_count'], submission_reviewers_count=task['submission_reviewers_count'],
max_attempts=task['max_attempts'], max_attempts=task.get('max_attempts'),
) )
competitions[i]['tasks'][j]['obj'] = task_obj competitions[i]['tasks'][j]['obj'] = task_obj
if task['type'] == CompetitionTask.CompetitionTaskType.INPUT.value: if task['type'] == CompetitionTask.CompetitionTaskType.INPUT.value:
task_obj.correct_answer_file = task['correct_answer_file'] task_obj.correct_answer_file = task['correct_answer_file']
if task.get("attachment"):
CompetitionTaskAttachment.objects.create(
task=task_obj,
file=task['attachment'],
bind_at=task['attachment_path'],
public=True
)
if ( if (
task['type'] task['type']
+3 -5
View File
@@ -5,6 +5,7 @@ from celery import shared_task
from django.conf import settings from django.conf import settings
from django.core.files.base import ContentFile from django.core.files.base import ContentFile
from urllib.parse import urlparse from urllib.parse import urlparse
import base64
from apps.task.models import CompetitionTaskSubmission from apps.task.models import CompetitionTaskSubmission
@@ -13,10 +14,7 @@ from apps.task.models import CompetitionTaskSubmission
def analyze_data_task(self, submission_id): def analyze_data_task(self, submission_id):
submission = CompetitionTaskSubmission.objects.get(id=submission_id) submission = CompetitionTaskSubmission.objects.get(id=submission_id)
try: try:
code_url = ( code = submission.content.read()
f"{settings.MINIO_DEFAULT_CUSTOM_ENDPOINT_URL}/"
f"{urlparse(submission.content.url).path}"
)
files = [ files = [
{ {
"url": ( "url": (
@@ -34,7 +32,7 @@ def analyze_data_task(self, submission_id):
f"{settings.CHECKER_API_ENDPOINT}/execute", f"{settings.CHECKER_API_ENDPOINT}/execute",
json={ json={
"files": files, "files": files,
"code_url": code_url, "code": base64.b64encode(code).decode("utf-8"),
"answer_file_path": submission.task.answer_file_path, "answer_file_path": submission.task.answer_file_path,
"expected_hash": hashlib.sha256( "expected_hash": hashlib.sha256(
submission.task.correct_answer_file.read() submission.task.correct_answer_file.read()
+1 -2
View File
@@ -2,10 +2,9 @@ from django.apps import AppConfig
class UsersConfig(AppConfig): class UsersConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "apps.user" name = "apps.user"
label = "user" label = "user"
verbose_name = "Пользователи (веб)" verbose_name = "контестанты"
def ready(self): def ready(self):
import apps.user.signals import apps.user.signals
@@ -30,12 +30,12 @@ export const Header = () => {
</Link> </Link>
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
<Link <a
to="/docs/" href="/docs/"
className="text-sm font-medium text-gray-700 hover:text-gray-900 transition-colors flex items-center gap-1" className="text-sm font-medium text-gray-700 hover:text-gray-900 transition-colors flex items-center gap-1"
> >
<FileText className="h-4 w-4" /> <FileText className="h-4 w-4" />
</Link> </a>
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
@@ -0,0 +1,88 @@
import React from 'react';
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogDescription,
} from "@/components/ui/dialog";
import { Loader2 } from 'lucide-react';
export interface CompetitionResult {
task_name: string;
result: number;
max_points: number
}
interface CompetitionResultsModalProps {
competitionTitle: string;
results: CompetitionResult[] | undefined;
isLoading: boolean;
error: unknown;
isOpen: boolean;
onOpenChange: (open: boolean) => void;
}
export const CompetitionResultsModal: React.FC<CompetitionResultsModalProps> = ({
competitionTitle,
results,
isLoading,
error,
isOpen,
onOpenChange,
}) => {
const renderResultValue = (result: number, maxPoints: number) => {
if (result === -1) {
return <span className="text-yellow-600">На проверке</span>;
} else if (result === -2) {
return <span className="text-gray-500">Нет ответа</span>;
} else {
return (
<span className="text-green-600">
Зачтено {result}/{maxPoints} баллов
</span>
);
}
};
return (
<Dialog open={isOpen} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-md md:max-w-lg">
<DialogHeader>
<DialogTitle>Результаты</DialogTitle>
<DialogDescription>
Ваши результаты по соревнованию "{competitionTitle}"
</DialogDescription>
</DialogHeader>
<div className="mt-4 space-y-4">
{isLoading ? (
<div className="flex justify-center py-8">
<Loader2 className="h-8 w-8 animate-spin text-gray-400" />
</div>
) : error ? (
<div className="text-center py-6 text-red-500">
Произошла ошибка при загрузке результатов
</div>
) : results && results.length > 0 ? (
results.map((result, index) => (
<div
key={index}
className="flex flex-col md:flex-row justify-between items-start md:items-center p-4 bg-gray-50 rounded-lg border"
>
<div className="font-medium mb-2 md:mb-0">{result.task_name}</div>
<div className="text-right font-semibold">
{renderResultValue(result.result, result.max_points)}
</div>
</div>
))
) : (
<div className="text-center py-6 text-gray-500">
Нет доступных результатов
</div>
)}
</div>
</DialogContent>
</Dialog>
);
};
@@ -1,20 +1,23 @@
import { useState } from "react";
import { useParams, Link, useNavigate } from "react-router-dom"; import { useParams, Link, useNavigate } from "react-router-dom";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { ArrowLeft, Clock, Trophy, BookOpen, BarChart2, AlertCircle } from "lucide-react"; import { ArrowLeft, Clock, Trophy, BookOpen, AlertCircle, BarChart2 } from "lucide-react";
import ReactMarkdown from "react-markdown"; import ReactMarkdown from "react-markdown";
import { useQuery, useMutation } from "@tanstack/react-query"; import { useQuery, useMutation } from "@tanstack/react-query";
import { getCompetition, startCompetition } from "@/shared/api/competitions"; import { getCompetition, startCompetition, getCompetitionResults } from "@/shared/api/competitions";
import { getCompetitionTasks } from "@/shared/api/session"; import { getCompetitionTasks } from "@/shared/api/session";
import { Loading } from "@/components/ui/loading"; import { Loading } from "@/components/ui/loading";
import { CompetitionType } from "@/shared/types/competition"; import { CompetitionType } from "@/shared/types/competition";
import remarkMath from "remark-math"; import remarkMath from "remark-math";
import remarkGfm from "remark-gfm"; import remarkGfm from "remark-gfm";
import rehypeKatex from "rehype-katex"; import rehypeKatex from "rehype-katex";
import { CompetitionResultsModal } from "./components/CompetitionResultModal";
const CompetitionPage = () => { const CompetitionPage = () => {
const { id } = useParams<{ id: string }>(); const { id } = useParams<{ id: string }>();
const navigate = useNavigate(); const navigate = useNavigate();
const competitionId = id || ""; const competitionId = id || "";
const [isResultsModalOpen, setIsResultsModalOpen] = useState(false);
const competitionQuery = useQuery({ const competitionQuery = useQuery({
queryKey: ["competition", competitionId], queryKey: ["competition", competitionId],
@@ -22,6 +25,12 @@ const CompetitionPage = () => {
enabled: !!competitionId, enabled: !!competitionId,
}); });
const resultsQuery = useQuery({
queryKey: ["competitionResults", competitionId],
queryFn: () => getCompetitionResults(competitionId),
enabled: !!competitionId,
});
const startMutation = useMutation({ const startMutation = useMutation({
mutationFn: () => startCompetition(competitionId), mutationFn: () => startCompetition(competitionId),
onSuccess: async () => { onSuccess: async () => {
@@ -61,9 +70,9 @@ const CompetitionPage = () => {
startMutation.mutate(); startMutation.mutate();
}; };
const handleViewResults = () => { const hasResults = resultsQuery.data &&
console.log("sorryan"); resultsQuery.data.length > 0 &&
}; resultsQuery.data.some(result => result.result !== -2);
if (competitionQuery.isLoading) { if (competitionQuery.isLoading) {
return <Loading />; return <Loading />;
@@ -75,15 +84,6 @@ const CompetitionPage = () => {
const competition = competitionQuery.data; const competition = competitionQuery.data;
const isCompetitionEnded = () => {
if (!competition?.end_date) return false;
const endDate = new Date(competition.end_date);
const now = new Date();
return now > endDate;
};
const isCompetitionNotStarted = () => { const isCompetitionNotStarted = () => {
if (!competition?.start_date) return false; if (!competition?.start_date) return false;
@@ -93,8 +93,18 @@ const CompetitionPage = () => {
return now < startDate; return now < startDate;
}; };
const competitionEnded = isCompetitionEnded(); // Check if competition has ended
const isCompetitionEnded = () => {
if (!competition?.end_date) return false;
const endDate = new Date(competition.end_date);
const now = new Date();
return now > endDate;
};
const competitionNotStarted = isCompetitionNotStarted(); const competitionNotStarted = isCompetitionNotStarted();
const competitionEnded = isCompetitionEnded();
return ( return (
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
@@ -133,17 +143,17 @@ const CompetitionPage = () => {
)} )}
</div> </div>
{competitionEnded && competition.type === CompetitionType.COMPETITIVE && (
<div className="bg-red-100 text-red-700 px-3 py-1 rounded-full text-sm font-medium">
Завершено
</div>
)}
{competitionNotStarted && competition.type === CompetitionType.COMPETITIVE && ( {competitionNotStarted && competition.type === CompetitionType.COMPETITIVE && (
<div className="bg-yellow-100 text-yellow-700 px-3 py-1 rounded-full text-sm font-medium"> <div className="bg-yellow-100 text-yellow-700 px-3 py-1 rounded-full text-sm font-medium">
Скоро начнется Скоро начнется
</div> </div>
)} )}
{competitionEnded && competition.type === CompetitionType.COMPETITIVE && (
<div className="bg-red-100 text-red-700 px-3 py-1 rounded-full text-sm font-medium">
Завершено
</div>
)}
</div> </div>
<h1 className="text-[34px] leading-11 font-semibold text-balance"> <h1 className="text-[34px] leading-11 font-semibold text-balance">
@@ -178,17 +188,8 @@ const CompetitionPage = () => {
</div> </div>
</div> </div>
<div className="w-full *:w-full md:w-96"> <div className="w-full *:w-full md:w-96 flex flex-col gap-3">
{competitionEnded && competition.type === CompetitionType.COMPETITIVE ? ( {competitionNotStarted && competition.type === CompetitionType.COMPETITIVE ? (
<Button
size={"lg"}
onClick={handleViewResults}
className="bg-indigo-600 hover:bg-indigo-700"
>
<BarChart2 size={18} className="mr-2" />
Смотреть результаты
</Button>
) : competitionNotStarted && competition.type === CompetitionType.COMPETITIVE ? (
<Button <Button
size={"lg"} size={"lg"}
disabled={true} disabled={true}
@@ -197,7 +198,7 @@ const CompetitionPage = () => {
<AlertCircle size={18} className="mr-2" /> <AlertCircle size={18} className="mr-2" />
Скоро начнется Скоро начнется
</Button> </Button>
) : ( ) : !competitionEnded ? (
<Button <Button
size={"lg"} size={"lg"}
onClick={handleStart} onClick={handleStart}
@@ -205,10 +206,36 @@ const CompetitionPage = () => {
> >
{startMutation.isPending ? "Загрузка..." : "Приступить к выполнению"} {startMutation.isPending ? "Загрузка..." : "Приступить к выполнению"}
</Button> </Button>
) : null}
{hasResults && (
<Button
size={"lg"}
onClick={() => setIsResultsModalOpen(true)}
className="bg-indigo-600 hover:bg-indigo-700"
>
<BarChart2 size={18} className="mr-2" />
Посмотреть результаты
</Button>
)}
{competitionEnded && !hasResults && competition.type === CompetitionType.COMPETITIVE && !resultsQuery.isLoading && (
<div className="text-center p-4 border rounded-md bg-gray-50">
<p className="text-gray-600">Соревнование завершено. Результаты пока не доступны.</p>
</div>
)} )}
</div> </div>
</div> </div>
</div> </div>
<CompetitionResultsModal
competitionTitle={competition.title}
results={resultsQuery.data}
isLoading={resultsQuery.isLoading}
error={resultsQuery.error}
isOpen={isResultsModalOpen}
onOpenChange={setIsResultsModalOpen}
/>
</div> </div>
); );
}; };
@@ -1,19 +1,21 @@
import React from 'react'; import React from 'react';
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Loader2 } from "lucide-react"; import { Loader2, CheckCircle } from "lucide-react";
interface ActionButtonsProps { interface ActionButtonsProps {
onSubmit: () => void; onSubmit: () => void;
onHistoryClick: () => void; onHistoryClick: () => void;
isSubmitting?: boolean; isSubmitting?: boolean;
hasSubmissionsLeft?: boolean; hasSubmissionsLeft?: boolean;
isCleared: boolean;
} }
const ActionButtons: React.FC<ActionButtonsProps> = ({ const ActionButtons: React.FC<ActionButtonsProps> = ({
onSubmit, onSubmit,
onHistoryClick, onHistoryClick,
isSubmitting = false, isSubmitting = false,
hasSubmissionsLeft = true hasSubmissionsLeft = true,
isCleared
}) => { }) => {
return ( return (
<div className="flex gap-8"> <div className="flex gap-8">
@@ -26,7 +28,15 @@ const ActionButtons: React.FC<ActionButtonsProps> = ({
История История
</Button> </Button>
{hasSubmissionsLeft ? ( {isCleared ? (
<Button
className="font-hse-sans flex-grow bg-green-600 hover:bg-green-700"
disabled={true}
>
<CheckCircle className="mr-2 h-4 w-4" />
Задача сдана!
</Button>
) : hasSubmissionsLeft ? (
<Button <Button
onClick={onSubmit} onClick={onSubmit}
className="font-hse-sans flex-grow" className="font-hse-sans flex-grow"
@@ -44,6 +44,10 @@ const TaskSolution: React.FC<TaskSolutionProps> = ({
const solutionHistory = solutionsQuery.data || []; const solutionHistory = solutionsQuery.data || [];
let lastSolutionPoints = 0;
if (solutionHistory.length > 0) {
lastSolutionPoints = solutionHistory[solutionHistory.length - 1].earned_points
}
const maxAttempts = task.max_attempts || -1; const maxAttempts = task.max_attempts || -1;
const submissionsUsed = solutionHistory.length; const submissionsUsed = solutionHistory.length;
const submissionsLeft = Math.max(0, maxAttempts - submissionsUsed); const submissionsLeft = Math.max(0, maxAttempts - submissionsUsed);
@@ -174,6 +178,7 @@ const TaskSolution: React.FC<TaskSolutionProps> = ({
onHistoryClick={handleOpenHistory} onHistoryClick={handleOpenHistory}
isSubmitting={isSubmitting} isSubmitting={isSubmitting}
hasSubmissionsLeft={hasSubmissionsLeft} hasSubmissionsLeft={hasSubmissionsLeft}
isCleared={task.points === lastSolutionPoints}
/> />
<SolutionHistorySheet <SolutionHistorySheet
@@ -1,5 +1,5 @@
import { userFetch } from "."; import { userFetch } from ".";
import { Competition } from "../types/competition"; import { Competition, CompetitionResult } from "../types/competition";
export const getCompetitions = async (participating?: boolean) => { export const getCompetitions = async (participating?: boolean) => {
return await userFetch<Competition[]>("/competitions", { return await userFetch<Competition[]>("/competitions", {
@@ -13,6 +13,10 @@ export const getCompetition = async (id: string) => {
return await userFetch<Competition>(`/competitions/${id}`); return await userFetch<Competition>(`/competitions/${id}`);
}; };
export const getCompetitionResults = async (id: string) => {
return await userFetch<CompetitionResult[]>(`/competitions/${id}/results`);
}
export const startCompetition = async (competitionId: string) => { export const startCompetition = async (competitionId: string) => {
return await userFetch(`/competitions/${competitionId}/start`, { return await userFetch(`/competitions/${competitionId}/start`, {
method: "POST", method: "POST",
@@ -24,3 +24,9 @@ export enum CompetitionType {
export enum CompetitionParticipationType { export enum CompetitionParticipationType {
SOLO = "solo", SOLO = "solo",
} }
export interface CompetitionResult {
task_name: string;
result: number;
max_points: number;
}