diff --git a/README.md b/README.md index 4a18e94..d3c0007 100644 --- a/README.md +++ b/README.md @@ -19,15 +19,17 @@ docker compose up * `/admin/grafana` - графана * `/docs` - обучающие материалы по анализу данных -После запуска по методу выше создается пользователь в админке (`/admin/`) с данными ниже:`admin` -- `admin` - логин -- `proooooood` - пароль +После запуска по методу выше создается пользователь в админке (`/admin/`) с данными ниже: +* `admin` - логин +* `proooooood` - пароль ## Тесты -Написаны unit-тесты (на базе Django TestCase) и E2E (Postman коллекция) +Написаны unit-тесты (на базе Django TestCase) и E2E (Postman коллекция). Они покрывают flow регистрации, просмотра и участия в соревновании. ![Postman data](img/postman.gif) -![django test]() +Ниже можно увидеть Coverage тестами бекенда данного приложения + +![django test](img/superduperdjangotests.png) diff --git a/img/superduperdjangotests.png b/img/superduperdjangotests.png new file mode 100644 index 0000000..d329283 Binary files /dev/null and b/img/superduperdjangotests.png differ diff --git a/services/backend/Dockerfile.staticfiles b/services/backend/Dockerfile.staticfiles index 21ac3a0..3916708 100644 --- a/services/backend/Dockerfile.staticfiles +++ b/services/backend/Dockerfile.staticfiles @@ -24,6 +24,6 @@ FROM docker.io/nginx:latest COPY --from=builder /app/static /usr/share/nginx/html -COPY ../checker/checker_requirements.txt . +COPY ../checker/checker_requirements.txt /usr/share/nginx/html CMD ["nginx", "-g", "daemon off;"] diff --git a/services/backend/api/v1/task/schemas.py b/services/backend/api/v1/task/schemas.py index 95f2fb4..ec8d80a 100644 --- a/services/backend/api/v1/task/schemas.py +++ b/services/backend/api/v1/task/schemas.py @@ -68,3 +68,4 @@ class TaskStatusSchema(Schema): task_name: str result: int max_points: int + position: int diff --git a/services/backend/api/v1/task/views.py b/services/backend/api/v1/task/views.py index a0ce64c..00aa38d 100644 --- a/services/backend/api/v1/task/views.py +++ b/services/backend/api/v1/task/views.py @@ -210,6 +210,7 @@ def get_competition_results(request, competition_id: UUID): task_name=task.title, result=result, max_points=task.points, + position=task.in_competition_position )) return status.OK, data 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 f44327f..1de2cb5 100644 --- a/services/backend/apps/core/management/commands/generate_pretty_data.py +++ b/services/backend/apps/core/management/commands/generate_pretty_data.py @@ -14,7 +14,8 @@ from apps.review.models import Reviewer from apps.task.models import ( CompetitionTask, CompetitionTaskCriteria, - CompetitionTaskSubmission, CompetitionTaskAttachment, + CompetitionTaskSubmission, + CompetitionTaskAttachment, ) from apps.user.models import User, UserRole @@ -44,6 +45,15 @@ dataset2 = ContentFile( name=f"dataset-{uuid.uuid4().hex}.csv", ) +correct_answer_file = ContentFile( + b"42", + name=f"answer.txt", +) +correct2_answer_file = ContentFile( + b"it is a dataset", + name=f"answer.txt", +) + now = timezone.now() image_dir = f"{settings.BASE_DIR}/apps/core/contents/images" @@ -103,8 +113,9 @@ E — коэффициент чувствительности количеств { "obj": None, "title": "Задача 2", - "description": "Найдите максимальную зарплату программиста из датасета на питоне", + "description": "Найдите максимальную зарплату программиста из датасета на питоне. Программа должна вывести содержимое файла по пути /dataset", "type": CompetitionTask.CompetitionTaskType.CHECKER.value, + "correct_answer_file": correct2_answer_file, "attachment": dataset, "attachment_path": "/dataset", "points": 25, @@ -317,9 +328,10 @@ B — пользователи, которым доступен только о и его характеристики. Однако оплатить товар можно и без захода на карточку товара. Задача: сравните группы по каждой метрике и сделайте вывод о том, стоит ли -продолжить внедрение обновленного магазина или нужно вернуть старый +продолжить внедрение обновленного магазина или нужно вернуть старый. Ответ 42. """.strip(), "type": CompetitionTask.CompetitionTaskType.CHECKER.value, + "correct_answer_file": correct_answer_file, "points": 30, "submission_reviewers_count": 2, }, @@ -338,7 +350,7 @@ B — пользователи, которым доступен только о "tasks": [ { "obj": None, - "title": "Анализ трендов", # TODO сюда добавить бд + "title": "Анализ трендов", # TODO сюда добавить бд "description": """ Скачайте базу данных со специальной страницы (https://dano.hse.ru/data), изучите ее более внимательно: посмотрите на переменные, посчитайте описательные статистики, постройте @@ -412,9 +424,8 @@ B — пользователи, которым доступен только о """.strip(), "type": CompetitionTask.CompetitionTaskType.INPUT.value, "points": 15, - "submission_reviewers_count": 2, "max_attempts": 50, - "correct_answer_file": ans3 + "correct_answer_file": ans3, }, { "obj": None, @@ -422,7 +433,6 @@ B — пользователи, которым доступен только о "description": "Сколько будет 6 * 7?", "type": CompetitionTask.CompetitionTaskType.INPUT.value, "points": 5, - "submission_reviewers_count": 2, "max_attempts": 10, "correct_answer_file": ans3, }, @@ -510,8 +520,8 @@ users = [ "role": UserRole.STUDENT.value, }, { - "email": "oleg-tinkov@gmail.com", - "username": "oleg-tinkov", + "email": "s.bliznyuk@tbank.ru", + "username": "s_bliznyuk", "password": "password123!", "role": UserRole.STUDENT.value, }, @@ -601,6 +611,7 @@ reviewers = [ }, ] + class Command(BaseCommand): help = "Generate sample data for Users, Competitions, Tasks, Submissions, and States." @@ -619,9 +630,9 @@ class Command(BaseCommand): def create_reviewers(self, count): reviewers_objs = [] for reviewer in reviewers: - name = reviewer['name'] - surname = reviewer['surname'] - token = reviewer['token'] + name = reviewer["name"] + surname = reviewer["surname"] + token = reviewer["token"] reviewer_obj = Reviewer(name=name, surname=surname, token=token) reviewer_obj.save() reviewers_objs.append(reviewer_obj) @@ -631,11 +642,11 @@ class Command(BaseCommand): users_objs = [] for user in users: user_obj, created = User.objects.get_or_create( - email=user['email'], + email=user["email"], defaults={ - "username": user['username'], - "password": make_password(user['password']), - "status": user['role'], + "username": user["username"], + "password": make_password(user["password"]), + "status": user["role"], }, ) users_objs.append(user_obj) @@ -646,20 +657,23 @@ class Command(BaseCommand): competitions_objs = [] for i, competition in enumerate(competitions): - competition_obj = Competition.objects.create( - title=competition['title'], - description=competition['description'], - start_date=competition['start_date'], - end_date=competition['end_date'], - type=competition['type'], - participation_type=competition['participation_type'], - ) + try: + competition_obj = Competition.objects.create( + title=competition["title"], + description=competition["description"], + start_date=competition["start_date"], + end_date=competition["end_date"], + type=competition["type"], + participation_type=competition["participation_type"], + ) + except Exception as e: + print(competition) if competition.get("image"): - competition_obj.image_url = competition['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) competitions_objs.append(competition_obj) self.stdout.write(f"Created competition: {competition['title']}") @@ -673,45 +687,53 @@ class Command(BaseCommand): CompetitionTask.CompetitionTaskType.INPUT.value, ] 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( - 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.get('max_attempts'), + 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" + ] if task["type"] == CompetitionTask.CompetitionTaskType.REVIEW.value else None, + correct_answer_file=task["correct_answer_file"] if task["type"] != CompetitionTask.CompetitionTaskType.REVIEW.value else None, + 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: - 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 + file=task["attachment"], + bind_at=task["attachment_path"], + public=True, ) if ( - task['type'] + task["type"] == CompetitionTask.CompetitionTaskType.REVIEW.value ): - for k, criteria in enumerate(task['criteries']): + for k, criteria in enumerate(task["criteries"]): criteria_obj = CompetitionTaskCriteria.objects.create( task=task_obj, - name=criteria['name'], - slug=criteria['slug'], - description=criteria['description'], - max_value=criteria['max_value'], + name=criteria["name"], + slug=criteria["slug"], + description=criteria["description"], + max_value=criteria["max_value"], + ) + competitions[i]["tasks"][j]["criteries"][k]["obj"] = ( + criteria_obj + ) + self.stdout.write( + f"Created criteria: {criteria['slug']}" ) - competitions[i]['tasks'][j]['criteries'][k]['obj'] = criteria_obj - self.stdout.write(f"Created criteria: {criteria['slug']}") tasks_objs.append(task_obj) - self.stdout.write(f"Created task: {task['title']} (type: {task['type']})") + self.stdout.write( + f"Created task: {task['title']} (type: {task['type']})" + ) self.add_reviewers_to_task(tasks_objs) return tasks_objs @@ -723,22 +745,29 @@ class Command(BaseCommand): def create_incorrect_submissions(self, tasks, users): for user in users: for task in tasks: - if task.type == CompetitionTask.CompetitionTaskType.REVIEW.value: + if ( + task.type + == CompetitionTask.CompetitionTaskType.REVIEW.value + ): num_submissions = random.randint(1, 3) for m in range(num_submissions): dummy_content_txt = ContentFile( - b"otvet: 112 sto proc" , + b"otvet: 112 sto proc", name=f"submission_{uuid.uuid4().hex}.txt", ) content_dir = f"{settings.BASE_DIR}/apps/core/contents" - with open(f"{content_dir}/presentation.pptx", "rb") as f: + with open( + f"{content_dir}/presentation.pptx", "rb" + ) as f: pptx = File(f, name="presentation.pptx") files = [pptx, pptx, dummy_content_txt] - submission = CompetitionTaskSubmission.objects.create( - user=user, - task=task, - content=random.choice(files), + submission = ( + CompetitionTaskSubmission.objects.create( + user=user, + task=task, + content=random.choice(files), + ) ) submission.save() submission.send_on_review() @@ -754,9 +783,10 @@ class Command(BaseCommand): competition=comp, defaults={ "state": "started", - "changed_at": timezone.now() - timedelta(days=random.randint(1, 30)), + "changed_at": timezone.now() + - timedelta(days=random.randint(1, 30)), }, ) 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/backend/apps/task/migrations/0004_alter_competitiontaskattachment_bind_at.py b/services/backend/apps/task/migrations/0004_alter_competitiontaskattachment_bind_at.py new file mode 100644 index 0000000..70dea88 --- /dev/null +++ b/services/backend/apps/task/migrations/0004_alter_competitiontaskattachment_bind_at.py @@ -0,0 +1,19 @@ +# Generated by Django 5.1.6 on 2025-03-03 23:02 + +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('task', '0003_alter_competitiontaskattachment_task'), + ] + + operations = [ + migrations.AlterField( + model_name='competitiontaskattachment', + name='bind_at', + field=models.CharField(max_length=255, validators=[django.core.validators.RegexValidator('^(?:[a-zA-Z]:\\\\(?:[^<>:\\"\\/\\\\|?*]*\\\\)*|/(?:[^<>:\\"\\/\\\\|?*]+/?)*)$', message='Введите абсолютный путь до папки')], verbose_name='путь сохранения'), + ), + ] diff --git a/services/backend/apps/task/models.py b/services/backend/apps/task/models.py index 75b0390..3752c34 100644 --- a/services/backend/apps/task/models.py +++ b/services/backend/apps/task/models.py @@ -91,7 +91,7 @@ class CompetitionTask(BaseModel): raise ValidationError({ "correct_answer_file": "Загрузите правильный ответ" }) - + # if self.answer_file_path and not self.type == "checker": # raise ValidationError({ # "type": "Укажите другой тип задания: этот не совместим с путем правильного ответа" @@ -100,7 +100,7 @@ class CompetitionTask(BaseModel): raise ValidationError({ "answer_file_path": "Введите путь правильного ответа - это нужно для корректной работы чекера" }) - + if not self.reviewers and self.type == "review": raise ValidationError({ "reviewers": "Загрузите ревьюверов - кто будет проверять задания, если не они?" @@ -110,7 +110,6 @@ class CompetitionTask(BaseModel): # "type": "Проверьте тип - вы ввели ревьюверов, но задание не является ручным" # }) - def __str__(self): return self.title @@ -150,9 +149,16 @@ class CompetitionTaskAttachment(BaseModel): related_name="attachments", ) file = models.FileField(upload_to=file_upload_at, verbose_name="файл") - bind_at = models.CharField(verbose_name="путь сохранения", max_length=255, - validators=[RegexValidator(r"^(?:[a-zA-Z]:\\(?:[^<>:\"\/\\|?*]*\\)*|/(?:[^<>:\"\/\\|?*]+/?)*)$", - message="Введите абсолютный путь до папки")]) + bind_at = models.CharField( + verbose_name="путь сохранения", + max_length=255, + validators=[ + RegexValidator( + r"^(?:[a-zA-Z]:\\(?:[^<>:\"\/\\|?*]*\\)*|/(?:[^<>:\"\/\\|?*]+/?)*)$", + message="Введите абсолютный путь до папки", + ) + ], + ) public = models.BooleanField(default=False, verbose_name="публичный") class Meta: diff --git a/services/backend/apps/task/tasks.py b/services/backend/apps/task/tasks.py index 522bf3a..713a75a 100644 --- a/services/backend/apps/task/tasks.py +++ b/services/backend/apps/task/tasks.py @@ -46,12 +46,12 @@ def analyze_data_task(self, submission_id): submission.stdout.save("output.txt", ContentFile(result["output"])) submission.result = { - "correct": result["correct"], + "correct": result["hash_match"], "hash_match": result["hash_match"], "error": result.get("error"), } submission.earned_points = ( - submission.task.points if result["correct"] else 0 + submission.task.points if result["hash_match"] else 0 ) submission.status = CompetitionTaskSubmission.StatusChoices.CHECKED diff --git a/services/backend/scripts/initdb b/services/backend/scripts/initdb index 146732a..7714b7c 100755 --- a/services/backend/scripts/initdb +++ b/services/backend/scripts/initdb @@ -10,4 +10,4 @@ if [ "$DJANGO_CREATE_SUPERUSER" = "True" ]; then python manage.py createsuperuser --noinput --username "$DJANGO_SUPERUSER_USERNAME" --email "$DJANGO_SUPERUSER_EMAIL" || true fi -python manage.py init_achievments \ No newline at end of file +python manage.py init_achievments diff --git a/services/frontend/src/pages/Competition/components/CompetitionResultModal/index.tsx b/services/frontend/src/pages/Competition/components/CompetitionResultModal/index.tsx index b691e77..080d9d6 100644 --- a/services/frontend/src/pages/Competition/components/CompetitionResultModal/index.tsx +++ b/services/frontend/src/pages/Competition/components/CompetitionResultModal/index.tsx @@ -1,4 +1,3 @@ -// src/components/competition/CompetitionResultsModal.tsx import React from 'react'; import { Dialog, @@ -12,7 +11,8 @@ import { Loader2 } from 'lucide-react'; export interface CompetitionResult { task_name: string; result: number; - max_points: number + max_points: number; + position: number; } interface CompetitionResultsModalProps { @@ -111,17 +111,19 @@ export const CompetitionResultsModal: React.FC = ( Произошла ошибка при загрузке результатов ) : results && results.length > 0 ? ( - results.map((result, index) => ( -
-
{result.task_name}
-
- {renderResultValue(result.result, result.max_points)} + [...results] + .sort((a, b) => a.position - b.position) + .map((result, index) => ( +
+
{result.task_name}
+
+ {renderResultValue(result.result, result.max_points)} +
-
- )) + )) ) : (
Нет доступных результатов diff --git a/services/frontend/src/pages/CompetitionSession/modules/TaskSolution/components/CodeSolution/index.tsx b/services/frontend/src/pages/CompetitionSession/modules/TaskSolution/components/CodeSolution/index.tsx index 11cc8a7..906d2ac 100644 --- a/services/frontend/src/pages/CompetitionSession/modules/TaskSolution/components/CodeSolution/index.tsx +++ b/services/frontend/src/pages/CompetitionSession/modules/TaskSolution/components/CodeSolution/index.tsx @@ -1,6 +1,13 @@ import React, { useRef, useEffect, useState } from 'react'; import * as monaco from 'monaco-editor'; -import { Copy, Check } from 'lucide-react'; +import { Copy, Check, Info } from 'lucide-react'; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog"; interface CodeSolutionProps { answer: string; @@ -92,16 +99,103 @@ const CodeSolution: React.FC = ({
{languageDisplay}
- +
+ + + + + + + Информация о среде выполнения + + +
+
+

Ограничение ресурсов

+
    +
  • +
    +
    +
    + Максимум 1 посылка в 10 секунд +
  • +
  • +
    +
    +
    + Максимальный размер решения 4MB +
  • +
  • +
    +
    +
    + Максимальное время работы программы 1 минута +
  • +
  • +
    +
    +
    + Выделяется 512MB на решение +
  • +
+
+ +
+

Доступные библиотеки

+
+
+
+ pandas + 2.2.3 +
+
+ numpy + 2.2.3 +
+
+ matplotlib + 3.10.1 +
+
+ scipy + 1.15.2 +
+
+ scikit-learn + 1.6.1 +
+
+ seaborn + 0.13.2 +
+
+ statsmodels + 0.14.4 +
+
+
+
+
+
+
+ + +
diff --git a/services/frontend/src/pages/CompetitionSession/modules/TaskSolution/index.tsx b/services/frontend/src/pages/CompetitionSession/modules/TaskSolution/index.tsx index 946939f..919da36 100644 --- a/services/frontend/src/pages/CompetitionSession/modules/TaskSolution/index.tsx +++ b/services/frontend/src/pages/CompetitionSession/modules/TaskSolution/index.tsx @@ -155,10 +155,10 @@ const TaskSolution: React.FC = ({ ? 'bg-blue-50 text-blue-700' : 'bg-red-50 text-red-700'}`} > - {hasSubmissionsLeft ? ( + {maxAttempts === -1 || hasSubmissionsLeft ? ( <> - Осталось посылок: {submissionsLeft === Infinity ? '∞' : submissionsLeft} + Осталось посылок: {maxAttempts === -1 ? '∞' : submissionsLeft} {maxAttempts !== -1 && (