diff --git a/README.md b/README.md index 6c44ca1..3659497 100644 --- a/README.md +++ b/README.md @@ -1 +1,33 @@ # 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]() diff --git a/img/postman.gif b/img/postman.gif new file mode 100644 index 0000000..7f62348 Binary files /dev/null and b/img/postman.gif differ diff --git a/services/backend/api/v1/task/schemas.py b/services/backend/api/v1/task/schemas.py index a203f22..95f2fb4 100644 --- a/services/backend/api/v1/task/schemas.py +++ b/services/backend/api/v1/task/schemas.py @@ -67,3 +67,4 @@ class TaskAttachmentSchema(ModelSchema): class TaskStatusSchema(Schema): task_name: str result: int + max_points: int diff --git a/services/backend/api/v1/task/views.py b/services/backend/api/v1/task/views.py index 7e16558..a0ce64c 100644 --- a/services/backend/api/v1/task/views.py +++ b/services/backend/api/v1/task/views.py @@ -195,14 +195,21 @@ def get_competition_results(request, competition_id: UUID): for task in tasks: submissions = CompetitionTaskSubmission.objects.filter( user=request.auth, task=task - ).filter(status="checked").all() + ).filter(status="checked").order_by("-earned_points").all() 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: result = submissions[0].earned_points data.append(TaskStatusSchema( task_name=task.title, - result=result + result=result, + max_points=task.points, )) return status.OK, data diff --git a/services/backend/apps/core/contents/images/dano.jpg b/services/backend/apps/core/contents/images/dano.jpg new file mode 100644 index 0000000..4211cba Binary files /dev/null and b/services/backend/apps/core/contents/images/dano.jpg differ diff --git a/services/backend/apps/core/contents/images/pikinevich.jpeg b/services/backend/apps/core/contents/images/pikinevich.jpeg new file mode 100644 index 0000000..8719ab7 Binary files /dev/null and b/services/backend/apps/core/contents/images/pikinevich.jpeg differ diff --git a/services/backend/apps/core/contents/images/shakhov.jpeg b/services/backend/apps/core/contents/images/shakhov.jpeg new file mode 100644 index 0000000..7e65a6a Binary files /dev/null and b/services/backend/apps/core/contents/images/shakhov.jpeg differ diff --git a/services/backend/apps/core/management/commands/generate_pretty_data.py b/services/backend/apps/core/management/commands/generate_pretty_data.py index 2a9da0a..bbcd67c 100644 --- a/services/backend/apps/core/management/commands/generate_pretty_data.py +++ b/services/backend/apps/core/management/commands/generate_pretty_data.py @@ -2,6 +2,7 @@ import random import uuid from datetime import timedelta, datetime +from PIL.Image import Image from django.conf import settings from django.contrib.auth.hashers import make_password from django.core.files.base import ContentFile, File @@ -13,10 +14,11 @@ from apps.review.models import Reviewer from apps.task.models import ( CompetitionTask, CompetitionTaskCriteria, - CompetitionTaskSubmission, + CompetitionTaskSubmission, CompetitionTaskAttachment, ) from apps.user.models import User, UserRole +# Примеры файлов с правильными ответами ans1 = ContentFile( b"1984", name=f"submission_{uuid.uuid4().hex}.txt", @@ -25,46 +27,86 @@ ans2 = ContentFile( b"3", 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() + +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 = [ { - "obj": None, # докидывает в процессе + "obj": None, # будет заполнено позже "title": "DANO. Финал", "description": "Олимпиада по анализу данных от Т-Банка и ВШЭ", "start_date": now - timedelta(days=2), "end_date": now + timedelta(days=5), "type": "competitive", "participation_type": "solo", + "image": dano_image, "tasks": [ { "obj": None, "title": "Задача 1", - "description": """На маркетплейсе «Е-шопинг» продаются различные товары. Одна из задач аналитика — -прогнозировать, сколько товаров будет продаваться при определенной цене. В ходе -исследований и экспериментов был выявлен вид зависимости: -$Q(P) = Q_0 \times e^{E \times \frac{P_0 - P}{P_0}}$ + "description": ( + """ +На маркетплейсе «Е-шопинг» продаются различные товары. Одна из задач аналитика — +прогнозировать, сколько товаров будет продаваться при определенной цене. В ходе исследований +был выявлен вид зависимости:\n +$Q(P) = Q_0 \\times e^{E \\times \\frac{P_0 - P}{P_0}}$\n где Q — это количество проданных единиц товара при цене P, Q 0 — количество проданных единиц товара при цене P0 , E — коэффициент чувствительности количества проданных единиц товара к изменению цены. +1. Кофемашину «Кофе каждый день» купили 200 000 раз (Q 0 ) при цене 20 000 ₽ (P 0 +). +Позже продавец поднял цену на 5 000 ₽, при этом продажи сократились на 24 000 +штук. Какой коэффициент чувствительности Е имеет этот товар? Ответ округлите +до двух знаков после запятой. +2. Потом продавец решил поставить новую цену на эту же модель: 22 000 ₽. Сколько +продаж согласно нашей зависимости будет у этого товара? Используйте результаты +предыдущего пункта. Ответ округлите до целых. +3. Другой продавец предлагает на нашем маркетплейсе кухонные ножи и сковородки. +Благодаря исследованиям были получены следующие формулы зависимостей +количества проданных товаров: Найдите, сколько заработает продавец при цене по 3 000 ₽ за нож и сковороду при условии, что себестоимость ножа — 1 000 ₽, а сковородки — 2 000 ₽.Ответ -округлите до целых.""".strip(), +округлите до целых. +Найдите, сколько заработает продавец при цене по 3 000 ₽ за нож и сковороду при условии, +что себестоимость ножа — 1 000 ₽, а сковородки — 2 000 ₽. Ответ округлите до целых. + """.strip() + ), "type": CompetitionTask.CompetitionTaskType.INPUT.value, "points": 3, "submission_reviewers_count": 2, "max_attempts": 20, - "correct_answer_file": ans1 + "correct_answer_file": ans1, }, { "obj": None, "title": "Задача 2", - "description": """ -Напишите "hello_dano" на питоне -""".strip(), + "description": "Найдите максимальную зарплату программиста из датасета на питоне", "type": CompetitionTask.CompetitionTaskType.CHECKER.value, + "attachment": dataset, + "attachment_path": "dataset", "points": 25, "submission_reviewers_count": 2, "max_attempts": 50, @@ -72,60 +114,66 @@ E — коэффициент чувствительности количеств { "obj": None, "title": "Задача 3", - "description": """Небольшой интернет-магазин собрал данные о действиях пользователей на своем сайте + "attachment": dataset2, + "attachment_path": "dataset2", + "description": """ +Небольшой интернет-магазин собрал данные о действиях пользователей на своем сайте за последние несколько месяцев. ecommerce_logs.csv — журнал действий пользователей: -• user_id — идентификатор пользователя. -• action — тип действия пользователя: -— visit — посещение сайта; -— click — клик на карточку товара; -— cart — добавление товара в корзину; -— delete — удаление товара из корзины; -— purchase — покупка товаров. -• date_time — время совершения действия. -• product_id — идентификатор товара. -• quantity — количество добавленного в корзину товара. -• delivery_price — стоимость доставки. -• sex — пол пользователя. -• region — регион пользователя. -• price — цена товара. - + • user_id — идентификатор пользователя. + • action — тип действия пользователя: + — visit — посещение сайта; + — click — клик на карточку товара; + — cart — добавление товара в корзину; + — delete — удаление товара из корзины; + — purchase — покупка товаров. + • date_time — время совершения действия. + • product_id — идентификатор товара. + • quantity — количество добавленного в корзину товара. + • delivery_price — стоимость доставки. + • sex — пол пользователя. + • region — регион пользователя. + • price — цена товара. +Ваша задача — проанализировать поведение пользователей, выявить возможные +проблемы при покупке и предложить решения. Ваш анализ поможет понять, на каком +этапе воронки магазин теряет покупателей и какие изменения можно внести, чтобы +улучшить процесс покупок в интернет-магазине. +Как правило, количество пользователей на каждом последующем шаге уменьшается, +и такая ситуация называется “воронкой”. Конверсия — это отношение количества +пользователей на каком-то одном шаге к количеству пользователей на одном +из предыдущих шагов. Например, конверсия из визита сайта в добавление товара +в корзину рассчитывается так: количество пользователей, добавивших товар в корзину, +делится на количество пользователей, посетивших сайт. Вам нужно изучить воронку конверсии, которая показывает, как пользователи переходят от одного шага к другому на сайте. В нашем случае воронка состоит из следующих шагов: 1. Посещение сайта. 2. Просмотр карточки товара. 3. Добавление товара в корзину. 4. Покупка. - -1. Посещение сайта. -2. Просмотр карточки товара. -3. Добавление товара в корзину. -4. Покупка. -3 / 11 1.) Посчитайте конверсию (округлите ответ до 3 знаков после запятой): -• Из визита на сайт в клик на карточку товара. -• Из клика в добавление в корзину. -• Из добавления в корзину в покупку. -• Из визита на сайт в добавление в корзину. -• Из визита на сайт в покупку. + • Из визита на сайт в клик на карточку товара. + • Из клика в добавление в корзину. + • Из добавления в корзину в покупку. + • Из визита на сайт в добавление в корзину. + • Из визита на сайт в покупку. 2. Постройте воронку конверсии с помощью столбчатой диаграммы: -• По оси X — шаги воронки. -• По оси Y — количество уникальных пользователей на каждом шаге. + • По оси X — шаги воронки. + • По оси Y — количество уникальных пользователей на каждом шаге. 3. Определите, на каком этапе конверсия из предыдущего шага ниже всего. Сформулируйте одну гипотезу, связанную с поведением пользователей, которая может объяснить падение конверсии именно на этом этапе. Обоснуйте механизм работы приведенной гипотезы. 4. Постройте график динамики (по оси X — дни) для каждой из конверсий: -• Конверсия из визита в клик. -• Конверсия из визита в добавление в корзину. -• Конверсия из визита в покупку. + • Конверсия из визита в клик. + • Конверсия из визита в добавление в корзину. + • Конверсия из визита в покупку. 5. На графике найдите просадку конверсии: укажите, какая конверсия просела и в какой примерно период это произошло (допустимая погрешность — 1–3 дня). 6. Чем вызвано снижение конверсии в этот период? Какие изменения в бизнесе или поведении пользователей могли бы объяснить это? Ответьте на оба вопроса, опираясь на данные. -""".strip(), + """.strip(), "type": CompetitionTask.CompetitionTaskType.REVIEW.value, "points": 10, "submission_reviewers_count": 2, @@ -136,18 +184,18 @@ ecommerce_logs.csv — журнал действий пользователей: "name": "Обоснованность решения", "slug": "validity", "description": "Аргументация", - "max_value": 5 + "max_value": 5, }, { "obj": None, "name": "Правильность", "slug": "correctness", - "description": "Насколько точные и верные ответы были представлены.", - "max_value": 5 - } - ] + "description": "Точность вычислений", + "max_value": 5, + }, + ], }, - ] + ], }, { "obj": None, @@ -161,53 +209,225 @@ ecommerce_logs.csv — журнал действий пользователей: { "obj": None, "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, "points": 3, "submission_reviewers_count": 2, - "max_attempts": 20, - "correct_answer_file": ans2 + "correct_answer_file": ans2, }, { "obj": None, "title": "Задача 2", "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, "points": 15, "submission_reviewers_count": 2, - "max_attempts": 1, "criteries": [ { "obj": None, "name": "Хорошесть отзыва", "slug": "validity", - "description": "Хорошесть", - "max_value": 10 + "description": "Критерий качества отзыва", + "max_value": 10, }, { "obj": None, "name": "Подробность", "slug": "detail", - "description": "Насколько подробно расписан ответ.", - "max_value": 5 - } - ] + "description": "Насколько подробно расписан ответ", + "max_value": 5, + }, + ], }, { "obj": None, "title": "Задача 3", "description": """ -Напишите выведите 1+3 на питоне -""".strip(), +Вы аналитик ведущей игровой компании 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(), "type": CompetitionTask.CompetitionTaskType.CHECKER.value, "points": 30, "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 = [ @@ -222,20 +442,163 @@ users = [ "username": "dreamonovich", "password": "password123!", "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 = [ { "name": "Владислав", "surname": "Пикиневич", - "token": "aa443163-9861-4b8d-b8f7-81ecd25f6088" + "token": "aa443163-9861-4b8d-b8f7-81ecd25f6088", }, { "name": "Александр", "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): @@ -251,6 +614,8 @@ class Command(BaseCommand): self.create_states(competitions, users) self.stdout.write("Data generation completed.") + f.close() + def create_reviewers(self, count): reviewers_objs = [] for reviewer in reviewers: @@ -290,6 +655,10 @@ class Command(BaseCommand): participation_type=competition['participation_type'], ) + if competition.get("image"): + competition_obj.image_url = competition['image'] + competition_obj.save() + competitions[i]['obj'] = competition_obj competition_obj.participants.add(*users) competitions_objs.append(competition_obj) @@ -306,19 +675,26 @@ class Command(BaseCommand): for i, competition in enumerate(competitions): for j, task in enumerate(competition['tasks']): task_obj = CompetitionTask.objects.create( - in_competition_position=j, + in_competition_position=j+1, competition=competition['obj'], title=task['title'], description=task['description'], type=task['type'], points=task['points'], 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 if task['type'] == CompetitionTask.CompetitionTaskType.INPUT.value: 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 ( task['type'] @@ -383,4 +759,4 @@ class Command(BaseCommand): ) self.stdout.write( f"Created state '{state_obj.state}' for user '{user.username}' in competition '{comp.title}'" - ) + ) \ No newline at end of file diff --git a/services/frontend/bun.lock b/services/frontend/bun.lock index 3448773..97110af 100644 --- a/services/frontend/bun.lock +++ b/services/frontend/bun.lock @@ -5,6 +5,7 @@ "name": "frontend", "dependencies": { "@monaco-editor/react": "^4.7.0", + "@radix-ui/react-accordion": "^1.2.3", "@radix-ui/react-dialog": "^1.1.6", "@radix-ui/react-dropdown-menu": "^2.1.6", "@radix-ui/react-label": "^2.1.2", @@ -154,8 +155,12 @@ "@radix-ui/primitive": ["@radix-ui/primitive@1.1.1", "", {}, "sha512-SJ31y+Q/zAyShtXJc8x83i9TYdbAfHZ++tUZnvjJJqFjzsdUnKsxPL6IEtBlxKkU7yzer//GQtZSV4GbldL3YA=="], + "@radix-ui/react-accordion": ["@radix-ui/react-accordion@1.2.3", "", { "dependencies": { "@radix-ui/primitive": "1.1.1", "@radix-ui/react-collapsible": "1.1.3", "@radix-ui/react-collection": "1.1.2", "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-context": "1.1.1", "@radix-ui/react-direction": "1.1.0", "@radix-ui/react-id": "1.1.0", "@radix-ui/react-primitive": "2.0.2", "@radix-ui/react-use-controllable-state": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-RIQ15mrcvqIkDARJeERSuXSry2N8uYnxkdDetpfmalT/+0ntOXLkFOsh9iwlAsCv+qcmhZjbdJogIm6WBa6c4A=="], + "@radix-ui/react-arrow": ["@radix-ui/react-arrow@1.1.2", "", { "dependencies": { "@radix-ui/react-primitive": "2.0.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-G+KcpzXHq24iH0uGG/pF8LyzpFJYGD4RfLjCIBfGdSLXvjLHST31RUiRVrupIBMvIppMgSzQ6l66iAxl03tdlg=="], + "@radix-ui/react-collapsible": ["@radix-ui/react-collapsible@1.1.3", "", { "dependencies": { "@radix-ui/primitive": "1.1.1", "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-context": "1.1.1", "@radix-ui/react-id": "1.1.0", "@radix-ui/react-presence": "1.1.2", "@radix-ui/react-primitive": "2.0.2", "@radix-ui/react-use-controllable-state": "1.1.0", "@radix-ui/react-use-layout-effect": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-jFSerheto1X03MUC0g6R7LedNW9EEGWdg9W1+MlpkMLwGkgkbUXLPBH/KIuWKXUoeYRVY11llqbTBDzuLg7qrw=="], + "@radix-ui/react-collection": ["@radix-ui/react-collection@1.1.2", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-context": "1.1.1", "@radix-ui/react-primitive": "2.0.2", "@radix-ui/react-slot": "1.1.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-9z54IEKRxIa9VityapoEYMuByaG42iSy1ZXlY2KcuLSEtq8x4987/N6m15ppoMffgZX72gER2uHe1D9Y6Unlcw=="], "@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Y9VzoRDSJtgFMUCoiZBDVo084VQ5hfpXxVE+NgkdNsjiDBByiImMZKKhxMwCbdHvhlENG6a833CbFkOQvTricw=="], diff --git a/services/frontend/package.json b/services/frontend/package.json index 98bd7bf..ebb93cd 100644 --- a/services/frontend/package.json +++ b/services/frontend/package.json @@ -11,6 +11,7 @@ }, "dependencies": { "@monaco-editor/react": "^4.7.0", + "@radix-ui/react-accordion": "^1.2.3", "@radix-ui/react-dialog": "^1.1.6", "@radix-ui/react-dropdown-menu": "^2.1.6", "@radix-ui/react-label": "^2.1.2", diff --git a/services/frontend/src/components/layout/header.tsx b/services/frontend/src/components/layout/header.tsx index 0f0cd86..ae5a16d 100644 --- a/services/frontend/src/components/layout/header.tsx +++ b/services/frontend/src/components/layout/header.tsx @@ -30,12 +30,12 @@ export const Header = () => {
- - Обучающие материалы - + + + diff --git a/services/frontend/src/components/ui/accordion.tsx b/services/frontend/src/components/ui/accordion.tsx new file mode 100644 index 0000000..8ad3ccc --- /dev/null +++ b/services/frontend/src/components/ui/accordion.tsx @@ -0,0 +1,64 @@ +import * as React from "react" +import * as AccordionPrimitive from "@radix-ui/react-accordion" +import { ChevronDownIcon } from "lucide-react" + +import { cn } from "@/shared/lib/utils" + +function Accordion({ + ...props +}: React.ComponentProps) { + return +} + +function AccordionItem({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AccordionTrigger({ + className, + children, + ...props +}: React.ComponentProps) { + return ( + + svg]:rotate-180", + className + )} + {...props} + > + {children} + + + + ) +} + +function AccordionContent({ + className, + children, + ...props +}: React.ComponentProps) { + return ( + +
{children}
+
+ ) +} + +export { Accordion, AccordionItem, AccordionTrigger, AccordionContent } diff --git a/services/frontend/src/pages/CompetitionSession/modules/TaskSolution/components/ActionButtons/index.tsx b/services/frontend/src/pages/CompetitionSession/modules/TaskSolution/components/ActionButtons/index.tsx index cf02e94..d22ae9c 100644 --- a/services/frontend/src/pages/CompetitionSession/modules/TaskSolution/components/ActionButtons/index.tsx +++ b/services/frontend/src/pages/CompetitionSession/modules/TaskSolution/components/ActionButtons/index.tsx @@ -1,19 +1,21 @@ import React from 'react'; import { Button } from "@/components/ui/button"; -import { Loader2 } from "lucide-react"; +import { Loader2, CheckCircle } from "lucide-react"; interface ActionButtonsProps { onSubmit: () => void; onHistoryClick: () => void; isSubmitting?: boolean; hasSubmissionsLeft?: boolean; + isCleared: boolean; } const ActionButtons: React.FC = ({ onSubmit, onHistoryClick, isSubmitting = false, - hasSubmissionsLeft = true + hasSubmissionsLeft = true, + isCleared }) => { return (
@@ -26,7 +28,15 @@ const ActionButtons: React.FC = ({ История - {hasSubmissionsLeft ? ( + {isCleared ? ( + + ) : hasSubmissionsLeft ? (