mirror of
https://gitlab.com/megazordpobeda/DataRush.git
synced 2026-05-23 14:27:10 +00:00
Merge branch 'master' of gitlab.prodcontest.ru:team-15/project
This commit is contained in:
@@ -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 коллекция)
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
![django test]()
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 261 KiB |
@@ -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
|
||||||
|
|||||||
@@ -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,60 +114,66 @@ 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 — идентификатор пользователя.
|
||||||
• action — тип действия пользователя:
|
• action — тип действия пользователя:
|
||||||
— visit — посещение сайта;
|
— visit — посещение сайта;
|
||||||
— click — клик на карточку товара;
|
— click — клик на карточку товара;
|
||||||
— cart — добавление товара в корзину;
|
— cart — добавление товара в корзину;
|
||||||
— delete — удаление товара из корзины;
|
— delete — удаление товара из корзины;
|
||||||
— purchase — покупка товаров.
|
— purchase — покупка товаров.
|
||||||
• date_time — время совершения действия.
|
• date_time — время совершения действия.
|
||||||
• product_id — идентификатор товара.
|
• product_id — идентификатор товара.
|
||||||
• quantity — количество добавленного в корзину товара.
|
• quantity — количество добавленного в корзину товара.
|
||||||
• delivery_price — стоимость доставки.
|
• delivery_price — стоимость доставки.
|
||||||
• sex — пол пользователя.
|
• sex — пол пользователя.
|
||||||
• region — регион пользователя.
|
• region — регион пользователя.
|
||||||
• price — цена товара.
|
• price — цена товара.
|
||||||
|
Ваша задача — проанализировать поведение пользователей, выявить возможные
|
||||||
|
проблемы при покупке и предложить решения. Ваш анализ поможет понять, на каком
|
||||||
|
этапе воронки магазин теряет покупателей и какие изменения можно внести, чтобы
|
||||||
|
улучшить процесс покупок в интернет-магазине.
|
||||||
|
Как правило, количество пользователей на каждом последующем шаге уменьшается,
|
||||||
|
и такая ситуация называется “воронкой”. Конверсия — это отношение количества
|
||||||
|
пользователей на каком-то одном шаге к количеству пользователей на одном
|
||||||
|
из предыдущих шагов. Например, конверсия из визита сайта в добавление товара
|
||||||
|
в корзину рассчитывается так: количество пользователей, добавивших товар в корзину,
|
||||||
|
делится на количество пользователей, посетивших сайт.
|
||||||
Вам нужно изучить воронку конверсии, которая показывает, как пользователи переходят
|
Вам нужно изучить воронку конверсии, которая показывает, как пользователи переходят
|
||||||
от одного шага к другому на сайте. В нашем случае воронка состоит из следующих шагов:
|
от одного шага к другому на сайте. В нашем случае воронка состоит из следующих шагов:
|
||||||
1. Посещение сайта.
|
1. Посещение сайта.
|
||||||
2. Просмотр карточки товара.
|
2. Просмотр карточки товара.
|
||||||
3. Добавление товара в корзину.
|
3. Добавление товара в корзину.
|
||||||
4. Покупка.
|
4. Покупка.
|
||||||
|
|
||||||
1. Посещение сайта.
|
|
||||||
2. Просмотр карточки товара.
|
|
||||||
3. Добавление товара в корзину.
|
|
||||||
4. Покупка.
|
|
||||||
3 / 11
|
|
||||||
1.) Посчитайте конверсию (округлите ответ до 3 знаков после запятой):
|
1.) Посчитайте конверсию (округлите ответ до 3 знаков после запятой):
|
||||||
• Из визита на сайт в клик на карточку товара.
|
• Из визита на сайт в клик на карточку товара.
|
||||||
• Из клика в добавление в корзину.
|
• Из клика в добавление в корзину.
|
||||||
• Из добавления в корзину в покупку.
|
• Из добавления в корзину в покупку.
|
||||||
• Из визита на сайт в добавление в корзину.
|
• Из визита на сайт в добавление в корзину.
|
||||||
• Из визита на сайт в покупку.
|
• Из визита на сайт в покупку.
|
||||||
2. Постройте воронку конверсии с помощью столбчатой диаграммы:
|
2. Постройте воронку конверсии с помощью столбчатой диаграммы:
|
||||||
• По оси X — шаги воронки.
|
• По оси X — шаги воронки.
|
||||||
• По оси Y — количество уникальных пользователей на каждом шаге.
|
• По оси Y — количество уникальных пользователей на каждом шаге.
|
||||||
3. Определите, на каком этапе конверсия из предыдущего шага ниже всего.
|
3. Определите, на каком этапе конверсия из предыдущего шага ниже всего.
|
||||||
Сформулируйте одну гипотезу, связанную с поведением пользователей, которая
|
Сформулируйте одну гипотезу, связанную с поведением пользователей, которая
|
||||||
может объяснить падение конверсии именно на этом этапе. Обоснуйте механизм
|
может объяснить падение конверсии именно на этом этапе. Обоснуйте механизм
|
||||||
работы приведенной гипотезы.
|
работы приведенной гипотезы.
|
||||||
4. Постройте график динамики (по оси X — дни) для каждой из конверсий:
|
4. Постройте график динамики (по оси X — дни) для каждой из конверсий:
|
||||||
• Конверсия из визита в клик.
|
• Конверсия из визита в клик.
|
||||||
• Конверсия из визита в добавление в корзину.
|
• Конверсия из визита в добавление в корзину.
|
||||||
• Конверсия из визита в покупку.
|
• Конверсия из визита в покупку.
|
||||||
5. На графике найдите просадку конверсии: укажите, какая конверсия просела
|
5. На графике найдите просадку конверсии: укажите, какая конверсия просела
|
||||||
и в какой примерно период это произошло (допустимая погрешность — 1–3
|
и в какой примерно период это произошло (допустимая погрешность — 1–3
|
||||||
дня).
|
дня).
|
||||||
6. Чем вызвано снижение конверсии в этот период? Какие изменения в бизнесе
|
6. Чем вызвано снижение конверсии в этот период? Какие изменения в бизнесе
|
||||||
или поведении пользователей могли бы объяснить это? Ответьте на оба
|
или поведении пользователей могли бы объяснить это? Ответьте на оба
|
||||||
вопроса, опираясь на данные.
|
вопроса, опираясь на данные.
|
||||||
""".strip(),
|
""".strip(),
|
||||||
"type": CompetitionTask.CompetitionTaskType.REVIEW.value,
|
"type": CompetitionTask.CompetitionTaskType.REVIEW.value,
|
||||||
"points": 10,
|
"points": 10,
|
||||||
"submission_reviewers_count": 2,
|
"submission_reviewers_count": 2,
|
||||||
@@ -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(Хороший)
|
Каждый день Дима звонит в пекарню, чтобы узнать, есть ли сегодня в продаже его
|
||||||
""".strip(),
|
любимые булочки с повидлом. За последние 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(),
|
||||||
"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., которая
|
||||||
""".strip(),
|
специализируется на разработке мобильных игр. Ваши коллеги разработали
|
||||||
|
обновленный игровой магазин, в котором игроки могут приобретать внутриигровые
|
||||||
|
предметы и суперспособности. Ваша задача — провести сравнение, чтобы
|
||||||
|
определить, как внедрение нового магазина повлияло на поведение пользователей
|
||||||
|
в игре.
|
||||||
|
Для этого пользователи были разделены на две равные группы случайным образом:
|
||||||
|
А — пользователи, которым доступен только старый магазин;
|
||||||
|
B — пользователи, которым доступен только обновленный магазин.
|
||||||
|
Спустя месяц после запуска по каждому пользователю из каждой группы были
|
||||||
|
посчитаны следующие метрики:
|
||||||
|
‘revenue_per_user’ — доход, который был получен от пользователя за период;
|
||||||
|
‘orders_cnt_per_user’ — количество заказов, которое совершено пользователем
|
||||||
|
за период;
|
||||||
|
‘converted_from_main_screen_to_item_card_screen’ — флаг захода на экран
|
||||||
|
с товарами (0 — если пользователь не заходил на карточку товара, 1 — если
|
||||||
|
заходил).
|
||||||
|
В таблице приведены значения этих метрик. Также в ней находится столбец ‘group’,
|
||||||
|
в котором указано, к какой группе (A или B) относится пользователь и столбец
|
||||||
|
‘period’ — характеризующий значение метрик до начала теста и во время проведения
|
||||||
|
теста. На карточке товара содержится дополнительная информация, фотография
|
||||||
|
и его характеристики. Однако оплатить товар можно и без захода на карточку
|
||||||
|
товара.
|
||||||
|
Задача: сравните группы по каждой метрике и сделайте вывод о том, стоит ли
|
||||||
|
продолжить внедрение обновленного магазина или нужно вернуть старый
|
||||||
|
""".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']
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
+13
-3
@@ -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;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user