mirror of
https://gitlab.com/megazordpobeda/DataRush.git
synced 2026-05-22 23:17:09 +00:00
Merge branch 'master' of gitlab.prodcontest.ru:team-15/project
This commit is contained in:
@@ -19,15 +19,17 @@ docker compose up
|
|||||||
* `/admin/grafana` - графана
|
* `/admin/grafana` - графана
|
||||||
* `/docs` - обучающие материалы по анализу данных
|
* `/docs` - обучающие материалы по анализу данных
|
||||||
|
|
||||||
После запуска по методу выше создается пользователь в админке (`/admin/`) с данными ниже:`admin`
|
После запуска по методу выше создается пользователь в админке (`/admin/`) с данными ниже:
|
||||||
- `admin` - логин
|
|
||||||
- `proooooood` - пароль
|
|
||||||
|
|
||||||
|
* `admin` - логин
|
||||||
|
* `proooooood` - пароль
|
||||||
|
|
||||||
## Тесты
|
## Тесты
|
||||||
|
|
||||||
Написаны unit-тесты (на базе Django TestCase) и E2E (Postman коллекция)
|
Написаны unit-тесты (на базе Django TestCase) и E2E (Postman коллекция). Они покрывают flow регистрации, просмотра и участия в соревновании.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
![django test]()
|
Ниже можно увидеть Coverage тестами бекенда данного приложения
|
||||||
|
|
||||||
|

|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 142 KiB |
@@ -24,6 +24,6 @@ FROM docker.io/nginx:latest
|
|||||||
|
|
||||||
COPY --from=builder /app/static /usr/share/nginx/html
|
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;"]
|
CMD ["nginx", "-g", "daemon off;"]
|
||||||
|
|||||||
@@ -68,3 +68,4 @@ class TaskStatusSchema(Schema):
|
|||||||
task_name: str
|
task_name: str
|
||||||
result: int
|
result: int
|
||||||
max_points: int
|
max_points: int
|
||||||
|
position: int
|
||||||
|
|||||||
@@ -210,6 +210,7 @@ def get_competition_results(request, competition_id: UUID):
|
|||||||
task_name=task.title,
|
task_name=task.title,
|
||||||
result=result,
|
result=result,
|
||||||
max_points=task.points,
|
max_points=task.points,
|
||||||
|
position=task.in_competition_position
|
||||||
))
|
))
|
||||||
|
|
||||||
return status.OK, data
|
return status.OK, data
|
||||||
|
|||||||
@@ -14,7 +14,8 @@ from apps.review.models import Reviewer
|
|||||||
from apps.task.models import (
|
from apps.task.models import (
|
||||||
CompetitionTask,
|
CompetitionTask,
|
||||||
CompetitionTaskCriteria,
|
CompetitionTaskCriteria,
|
||||||
CompetitionTaskSubmission, CompetitionTaskAttachment,
|
CompetitionTaskSubmission,
|
||||||
|
CompetitionTaskAttachment,
|
||||||
)
|
)
|
||||||
from apps.user.models import User, UserRole
|
from apps.user.models import User, UserRole
|
||||||
|
|
||||||
@@ -44,6 +45,15 @@ dataset2 = ContentFile(
|
|||||||
name=f"dataset-{uuid.uuid4().hex}.csv",
|
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()
|
now = timezone.now()
|
||||||
|
|
||||||
image_dir = f"{settings.BASE_DIR}/apps/core/contents/images"
|
image_dir = f"{settings.BASE_DIR}/apps/core/contents/images"
|
||||||
@@ -103,8 +113,9 @@ E — коэффициент чувствительности количеств
|
|||||||
{
|
{
|
||||||
"obj": None,
|
"obj": None,
|
||||||
"title": "Задача 2",
|
"title": "Задача 2",
|
||||||
"description": "Найдите максимальную зарплату программиста из датасета на питоне",
|
"description": "Найдите максимальную зарплату программиста из датасета на питоне. Программа должна вывести содержимое файла по пути /dataset",
|
||||||
"type": CompetitionTask.CompetitionTaskType.CHECKER.value,
|
"type": CompetitionTask.CompetitionTaskType.CHECKER.value,
|
||||||
|
"correct_answer_file": correct2_answer_file,
|
||||||
"attachment": dataset,
|
"attachment": dataset,
|
||||||
"attachment_path": "/dataset",
|
"attachment_path": "/dataset",
|
||||||
"points": 25,
|
"points": 25,
|
||||||
@@ -317,9 +328,10 @@ B — пользователи, которым доступен только о
|
|||||||
и его характеристики. Однако оплатить товар можно и без захода на карточку
|
и его характеристики. Однако оплатить товар можно и без захода на карточку
|
||||||
товара.
|
товара.
|
||||||
Задача: сравните группы по каждой метрике и сделайте вывод о том, стоит ли
|
Задача: сравните группы по каждой метрике и сделайте вывод о том, стоит ли
|
||||||
продолжить внедрение обновленного магазина или нужно вернуть старый
|
продолжить внедрение обновленного магазина или нужно вернуть старый. Ответ 42.
|
||||||
""".strip(),
|
""".strip(),
|
||||||
"type": CompetitionTask.CompetitionTaskType.CHECKER.value,
|
"type": CompetitionTask.CompetitionTaskType.CHECKER.value,
|
||||||
|
"correct_answer_file": correct_answer_file,
|
||||||
"points": 30,
|
"points": 30,
|
||||||
"submission_reviewers_count": 2,
|
"submission_reviewers_count": 2,
|
||||||
},
|
},
|
||||||
@@ -338,7 +350,7 @@ B — пользователи, которым доступен только о
|
|||||||
"tasks": [
|
"tasks": [
|
||||||
{
|
{
|
||||||
"obj": None,
|
"obj": None,
|
||||||
"title": "Анализ трендов", # TODO сюда добавить бд
|
"title": "Анализ трендов", # TODO сюда добавить бд
|
||||||
"description": """
|
"description": """
|
||||||
Скачайте базу данных со специальной страницы (https://dano.hse.ru/data), изучите ее более
|
Скачайте базу данных со специальной страницы (https://dano.hse.ru/data), изучите ее более
|
||||||
внимательно: посмотрите на переменные, посчитайте описательные статистики, постройте
|
внимательно: посмотрите на переменные, посчитайте описательные статистики, постройте
|
||||||
@@ -412,9 +424,8 @@ B — пользователи, которым доступен только о
|
|||||||
""".strip(),
|
""".strip(),
|
||||||
"type": CompetitionTask.CompetitionTaskType.INPUT.value,
|
"type": CompetitionTask.CompetitionTaskType.INPUT.value,
|
||||||
"points": 15,
|
"points": 15,
|
||||||
"submission_reviewers_count": 2,
|
|
||||||
"max_attempts": 50,
|
"max_attempts": 50,
|
||||||
"correct_answer_file": ans3
|
"correct_answer_file": ans3,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"obj": None,
|
"obj": None,
|
||||||
@@ -422,7 +433,6 @@ B — пользователи, которым доступен только о
|
|||||||
"description": "Сколько будет 6 * 7?",
|
"description": "Сколько будет 6 * 7?",
|
||||||
"type": CompetitionTask.CompetitionTaskType.INPUT.value,
|
"type": CompetitionTask.CompetitionTaskType.INPUT.value,
|
||||||
"points": 5,
|
"points": 5,
|
||||||
"submission_reviewers_count": 2,
|
|
||||||
"max_attempts": 10,
|
"max_attempts": 10,
|
||||||
"correct_answer_file": ans3,
|
"correct_answer_file": ans3,
|
||||||
},
|
},
|
||||||
@@ -510,8 +520,8 @@ users = [
|
|||||||
"role": UserRole.STUDENT.value,
|
"role": UserRole.STUDENT.value,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"email": "oleg-tinkov@gmail.com",
|
"email": "s.bliznyuk@tbank.ru",
|
||||||
"username": "oleg-tinkov",
|
"username": "s_bliznyuk",
|
||||||
"password": "password123!",
|
"password": "password123!",
|
||||||
"role": UserRole.STUDENT.value,
|
"role": UserRole.STUDENT.value,
|
||||||
},
|
},
|
||||||
@@ -601,6 +611,7 @@ reviewers = [
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class Command(BaseCommand):
|
class Command(BaseCommand):
|
||||||
help = "Generate sample data for Users, Competitions, Tasks, Submissions, and States."
|
help = "Generate sample data for Users, Competitions, Tasks, Submissions, and States."
|
||||||
|
|
||||||
@@ -619,9 +630,9 @@ class Command(BaseCommand):
|
|||||||
def create_reviewers(self, count):
|
def create_reviewers(self, count):
|
||||||
reviewers_objs = []
|
reviewers_objs = []
|
||||||
for reviewer in reviewers:
|
for reviewer in reviewers:
|
||||||
name = reviewer['name']
|
name = reviewer["name"]
|
||||||
surname = reviewer['surname']
|
surname = reviewer["surname"]
|
||||||
token = reviewer['token']
|
token = reviewer["token"]
|
||||||
reviewer_obj = Reviewer(name=name, surname=surname, token=token)
|
reviewer_obj = Reviewer(name=name, surname=surname, token=token)
|
||||||
reviewer_obj.save()
|
reviewer_obj.save()
|
||||||
reviewers_objs.append(reviewer_obj)
|
reviewers_objs.append(reviewer_obj)
|
||||||
@@ -631,11 +642,11 @@ class Command(BaseCommand):
|
|||||||
users_objs = []
|
users_objs = []
|
||||||
for user in users:
|
for user in users:
|
||||||
user_obj, created = User.objects.get_or_create(
|
user_obj, created = User.objects.get_or_create(
|
||||||
email=user['email'],
|
email=user["email"],
|
||||||
defaults={
|
defaults={
|
||||||
"username": user['username'],
|
"username": user["username"],
|
||||||
"password": make_password(user['password']),
|
"password": make_password(user["password"]),
|
||||||
"status": user['role'],
|
"status": user["role"],
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
users_objs.append(user_obj)
|
users_objs.append(user_obj)
|
||||||
@@ -646,20 +657,23 @@ class Command(BaseCommand):
|
|||||||
competitions_objs = []
|
competitions_objs = []
|
||||||
|
|
||||||
for i, competition in enumerate(competitions):
|
for i, competition in enumerate(competitions):
|
||||||
competition_obj = Competition.objects.create(
|
try:
|
||||||
title=competition['title'],
|
competition_obj = Competition.objects.create(
|
||||||
description=competition['description'],
|
title=competition["title"],
|
||||||
start_date=competition['start_date'],
|
description=competition["description"],
|
||||||
end_date=competition['end_date'],
|
start_date=competition["start_date"],
|
||||||
type=competition['type'],
|
end_date=competition["end_date"],
|
||||||
participation_type=competition['participation_type'],
|
type=competition["type"],
|
||||||
)
|
participation_type=competition["participation_type"],
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
print(competition)
|
||||||
|
|
||||||
if competition.get("image"):
|
if competition.get("image"):
|
||||||
competition_obj.image_url = competition['image']
|
competition_obj.image_url = competition["image"]
|
||||||
competition_obj.save()
|
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)
|
||||||
self.stdout.write(f"Created competition: {competition['title']}")
|
self.stdout.write(f"Created competition: {competition['title']}")
|
||||||
@@ -673,45 +687,53 @@ class Command(BaseCommand):
|
|||||||
CompetitionTask.CompetitionTaskType.INPUT.value,
|
CompetitionTask.CompetitionTaskType.INPUT.value,
|
||||||
]
|
]
|
||||||
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+1,
|
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[
|
||||||
max_attempts=task.get('max_attempts'),
|
"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"):
|
if task.get("attachment"):
|
||||||
CompetitionTaskAttachment.objects.create(
|
CompetitionTaskAttachment.objects.create(
|
||||||
task=task_obj,
|
task=task_obj,
|
||||||
file=task['attachment'],
|
file=task["attachment"],
|
||||||
bind_at=task['attachment_path'],
|
bind_at=task["attachment_path"],
|
||||||
public=True
|
public=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
if (
|
if (
|
||||||
task['type']
|
task["type"]
|
||||||
== CompetitionTask.CompetitionTaskType.REVIEW.value
|
== CompetitionTask.CompetitionTaskType.REVIEW.value
|
||||||
):
|
):
|
||||||
for k, criteria in enumerate(task['criteries']):
|
for k, criteria in enumerate(task["criteries"]):
|
||||||
criteria_obj = CompetitionTaskCriteria.objects.create(
|
criteria_obj = CompetitionTaskCriteria.objects.create(
|
||||||
task=task_obj,
|
task=task_obj,
|
||||||
name=criteria['name'],
|
name=criteria["name"],
|
||||||
slug=criteria['slug'],
|
slug=criteria["slug"],
|
||||||
description=criteria['description'],
|
description=criteria["description"],
|
||||||
max_value=criteria['max_value'],
|
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)
|
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)
|
self.add_reviewers_to_task(tasks_objs)
|
||||||
return tasks_objs
|
return tasks_objs
|
||||||
|
|
||||||
@@ -723,22 +745,29 @@ class Command(BaseCommand):
|
|||||||
def create_incorrect_submissions(self, tasks, users):
|
def create_incorrect_submissions(self, tasks, users):
|
||||||
for user in users:
|
for user in users:
|
||||||
for task in tasks:
|
for task in tasks:
|
||||||
if task.type == CompetitionTask.CompetitionTaskType.REVIEW.value:
|
if (
|
||||||
|
task.type
|
||||||
|
== CompetitionTask.CompetitionTaskType.REVIEW.value
|
||||||
|
):
|
||||||
num_submissions = random.randint(1, 3)
|
num_submissions = random.randint(1, 3)
|
||||||
for m in range(num_submissions):
|
for m in range(num_submissions):
|
||||||
dummy_content_txt = ContentFile(
|
dummy_content_txt = ContentFile(
|
||||||
b"otvet: 112 sto proc" ,
|
b"otvet: 112 sto proc",
|
||||||
name=f"submission_{uuid.uuid4().hex}.txt",
|
name=f"submission_{uuid.uuid4().hex}.txt",
|
||||||
)
|
)
|
||||||
|
|
||||||
content_dir = f"{settings.BASE_DIR}/apps/core/contents"
|
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")
|
pptx = File(f, name="presentation.pptx")
|
||||||
files = [pptx, pptx, dummy_content_txt]
|
files = [pptx, pptx, dummy_content_txt]
|
||||||
submission = CompetitionTaskSubmission.objects.create(
|
submission = (
|
||||||
user=user,
|
CompetitionTaskSubmission.objects.create(
|
||||||
task=task,
|
user=user,
|
||||||
content=random.choice(files),
|
task=task,
|
||||||
|
content=random.choice(files),
|
||||||
|
)
|
||||||
)
|
)
|
||||||
submission.save()
|
submission.save()
|
||||||
submission.send_on_review()
|
submission.send_on_review()
|
||||||
@@ -754,9 +783,10 @@ class Command(BaseCommand):
|
|||||||
competition=comp,
|
competition=comp,
|
||||||
defaults={
|
defaults={
|
||||||
"state": "started",
|
"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(
|
self.stdout.write(
|
||||||
f"Created state '{state_obj.state}' for user '{user.username}' in competition '{comp.title}'"
|
f"Created state '{state_obj.state}' for user '{user.username}' in competition '{comp.title}'"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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='путь сохранения'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -91,7 +91,7 @@ class CompetitionTask(BaseModel):
|
|||||||
raise ValidationError({
|
raise ValidationError({
|
||||||
"correct_answer_file": "Загрузите правильный ответ"
|
"correct_answer_file": "Загрузите правильный ответ"
|
||||||
})
|
})
|
||||||
|
|
||||||
# if self.answer_file_path and not self.type == "checker":
|
# if self.answer_file_path and not self.type == "checker":
|
||||||
# raise ValidationError({
|
# raise ValidationError({
|
||||||
# "type": "Укажите другой тип задания: этот не совместим с путем правильного ответа"
|
# "type": "Укажите другой тип задания: этот не совместим с путем правильного ответа"
|
||||||
@@ -100,7 +100,7 @@ class CompetitionTask(BaseModel):
|
|||||||
raise ValidationError({
|
raise ValidationError({
|
||||||
"answer_file_path": "Введите путь правильного ответа - это нужно для корректной работы чекера"
|
"answer_file_path": "Введите путь правильного ответа - это нужно для корректной работы чекера"
|
||||||
})
|
})
|
||||||
|
|
||||||
if not self.reviewers and self.type == "review":
|
if not self.reviewers and self.type == "review":
|
||||||
raise ValidationError({
|
raise ValidationError({
|
||||||
"reviewers": "Загрузите ревьюверов - кто будет проверять задания, если не они?"
|
"reviewers": "Загрузите ревьюверов - кто будет проверять задания, если не они?"
|
||||||
@@ -110,7 +110,6 @@ class CompetitionTask(BaseModel):
|
|||||||
# "type": "Проверьте тип - вы ввели ревьюверов, но задание не является ручным"
|
# "type": "Проверьте тип - вы ввели ревьюверов, но задание не является ручным"
|
||||||
# })
|
# })
|
||||||
|
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.title
|
return self.title
|
||||||
|
|
||||||
@@ -150,9 +149,16 @@ class CompetitionTaskAttachment(BaseModel):
|
|||||||
related_name="attachments",
|
related_name="attachments",
|
||||||
)
|
)
|
||||||
file = models.FileField(upload_to=file_upload_at, verbose_name="файл")
|
file = models.FileField(upload_to=file_upload_at, verbose_name="файл")
|
||||||
bind_at = models.CharField(verbose_name="путь сохранения", max_length=255,
|
bind_at = models.CharField(
|
||||||
validators=[RegexValidator(r"^(?:[a-zA-Z]:\\(?:[^<>:\"\/\\|?*]*\\)*|/(?:[^<>:\"\/\\|?*]+/?)*)$",
|
verbose_name="путь сохранения",
|
||||||
message="Введите абсолютный путь до папки")])
|
max_length=255,
|
||||||
|
validators=[
|
||||||
|
RegexValidator(
|
||||||
|
r"^(?:[a-zA-Z]:\\(?:[^<>:\"\/\\|?*]*\\)*|/(?:[^<>:\"\/\\|?*]+/?)*)$",
|
||||||
|
message="Введите абсолютный путь до папки",
|
||||||
|
)
|
||||||
|
],
|
||||||
|
)
|
||||||
public = models.BooleanField(default=False, verbose_name="публичный")
|
public = models.BooleanField(default=False, verbose_name="публичный")
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|||||||
@@ -46,12 +46,12 @@ def analyze_data_task(self, submission_id):
|
|||||||
|
|
||||||
submission.stdout.save("output.txt", ContentFile(result["output"]))
|
submission.stdout.save("output.txt", ContentFile(result["output"]))
|
||||||
submission.result = {
|
submission.result = {
|
||||||
"correct": result["correct"],
|
"correct": result["hash_match"],
|
||||||
"hash_match": result["hash_match"],
|
"hash_match": result["hash_match"],
|
||||||
"error": result.get("error"),
|
"error": result.get("error"),
|
||||||
}
|
}
|
||||||
submission.earned_points = (
|
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
|
submission.status = CompetitionTaskSubmission.StatusChoices.CHECKED
|
||||||
|
|
||||||
|
|||||||
@@ -10,4 +10,4 @@ if [ "$DJANGO_CREATE_SUPERUSER" = "True" ]; then
|
|||||||
python manage.py createsuperuser --noinput --username "$DJANGO_SUPERUSER_USERNAME" --email "$DJANGO_SUPERUSER_EMAIL" || true
|
python manage.py createsuperuser --noinput --username "$DJANGO_SUPERUSER_USERNAME" --email "$DJANGO_SUPERUSER_EMAIL" || true
|
||||||
fi
|
fi
|
||||||
|
|
||||||
python manage.py init_achievments
|
python manage.py init_achievments
|
||||||
|
|||||||
+14
-12
@@ -1,4 +1,3 @@
|
|||||||
// src/components/competition/CompetitionResultsModal.tsx
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
@@ -12,7 +11,8 @@ import { Loader2 } from 'lucide-react';
|
|||||||
export interface CompetitionResult {
|
export interface CompetitionResult {
|
||||||
task_name: string;
|
task_name: string;
|
||||||
result: number;
|
result: number;
|
||||||
max_points: number
|
max_points: number;
|
||||||
|
position: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface CompetitionResultsModalProps {
|
interface CompetitionResultsModalProps {
|
||||||
@@ -111,17 +111,19 @@ export const CompetitionResultsModal: React.FC<CompetitionResultsModalProps> = (
|
|||||||
Произошла ошибка при загрузке результатов
|
Произошла ошибка при загрузке результатов
|
||||||
</div>
|
</div>
|
||||||
) : results && results.length > 0 ? (
|
) : results && results.length > 0 ? (
|
||||||
results.map((result, index) => (
|
[...results]
|
||||||
<div
|
.sort((a, b) => a.position - b.position)
|
||||||
key={index}
|
.map((result, index) => (
|
||||||
className="flex flex-col md:flex-row justify-between items-start md:items-center p-4 bg-gray-50 rounded-lg border"
|
<div
|
||||||
>
|
key={index}
|
||||||
<div className="font-medium mb-2 md:mb-0">{result.task_name}</div>
|
className="flex flex-col md:flex-row justify-between items-start md:items-center p-4 bg-gray-50 rounded-lg border"
|
||||||
<div className="text-right">
|
>
|
||||||
{renderResultValue(result.result, result.max_points)}
|
<div className="font-medium mb-2 md:mb-0">{result.task_name}</div>
|
||||||
|
<div className="text-right">
|
||||||
|
{renderResultValue(result.result, result.max_points)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
))
|
||||||
))
|
|
||||||
) : (
|
) : (
|
||||||
<div className="text-center py-6 text-gray-500">
|
<div className="text-center py-6 text-gray-500">
|
||||||
Нет доступных результатов
|
Нет доступных результатов
|
||||||
|
|||||||
+105
-11
@@ -1,6 +1,13 @@
|
|||||||
import React, { useRef, useEffect, useState } from 'react';
|
import React, { useRef, useEffect, useState } from 'react';
|
||||||
import * as monaco from 'monaco-editor';
|
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 {
|
interface CodeSolutionProps {
|
||||||
answer: string;
|
answer: string;
|
||||||
@@ -92,16 +99,103 @@ const CodeSolution: React.FC<CodeSolutionProps> = ({
|
|||||||
<div className="bg-white rounded-lg overflow-hidden border border-gray-200">
|
<div className="bg-white rounded-lg overflow-hidden border border-gray-200">
|
||||||
<div className="flex items-center justify-between bg-gray-50 px-4 py-2 border-b border-gray-200">
|
<div className="flex items-center justify-between bg-gray-50 px-4 py-2 border-b border-gray-200">
|
||||||
<div className="text-sm font-medium text-gray-600">{languageDisplay}</div>
|
<div className="text-sm font-medium text-gray-600">{languageDisplay}</div>
|
||||||
<button
|
<div className="flex items-center space-x-3">
|
||||||
onClick={copyToClipboard}
|
<Dialog>
|
||||||
className="flex items-center text-sm text-gray-500 hover:text-gray-700 transition-colors"
|
<DialogTrigger asChild>
|
||||||
>
|
<button
|
||||||
{copied ? (
|
className="flex items-center text-sm text-gray-500 hover:text-gray-700 transition-colors"
|
||||||
<Check className="w-4 h-4 mr-1" />
|
title="Информация о среде выполнения"
|
||||||
) : (
|
>
|
||||||
<Copy className="w-4 h-4 mr-1" />
|
<Info className="w-4 h-4" />
|
||||||
)}
|
</button>
|
||||||
</button>
|
</DialogTrigger>
|
||||||
|
<DialogContent className="sm:max-w-md">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle className="text-xl font-semibold">Информация о среде выполнения</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<div className="mt-4 space-y-6">
|
||||||
|
<div>
|
||||||
|
<h3 className="text-lg font-semibold mb-3 border-b pb-2">Ограничение ресурсов</h3>
|
||||||
|
<ul className="space-y-3 text-gray-700">
|
||||||
|
<li className="flex items-start">
|
||||||
|
<div className="bg-yellow-100 p-1.5 rounded-full mr-3 mt-0.5">
|
||||||
|
<div className="w-1.5 h-1.5 bg-yellow-500 rounded-full"></div>
|
||||||
|
</div>
|
||||||
|
Максимум 1 посылка в 10 секунд
|
||||||
|
</li>
|
||||||
|
<li className="flex items-start">
|
||||||
|
<div className="bg-yellow-100 p-1.5 rounded-full mr-3 mt-0.5">
|
||||||
|
<div className="w-1.5 h-1.5 bg-yellow-500 rounded-full"></div>
|
||||||
|
</div>
|
||||||
|
Максимальный размер решения 4MB
|
||||||
|
</li>
|
||||||
|
<li className="flex items-start">
|
||||||
|
<div className="bg-yellow-100 p-1.5 rounded-full mr-3 mt-0.5">
|
||||||
|
<div className="w-1.5 h-1.5 bg-yellow-500 rounded-full"></div>
|
||||||
|
</div>
|
||||||
|
Максимальное время работы программы 1 минута
|
||||||
|
</li>
|
||||||
|
<li className="flex items-start">
|
||||||
|
<div className="bg-yellow-100 p-1.5 rounded-full mr-3 mt-0.5">
|
||||||
|
<div className="w-1.5 h-1.5 bg-yellow-500 rounded-full"></div>
|
||||||
|
</div>
|
||||||
|
Выделяется 512MB на решение
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h3 className="text-lg font-semibold mb-3 border-b pb-2">Доступные библиотеки</h3>
|
||||||
|
<div className="bg-gray-50 p-4 rounded-md font-mono text-sm">
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-2">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<span className="text-yellow-600 font-semibold">pandas</span>
|
||||||
|
<span className="text-gray-500 ml-2">2.2.3</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<span className="text-yellow-600 font-semibold">numpy</span>
|
||||||
|
<span className="text-gray-500 ml-2">2.2.3</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<span className="text-yellow-600 font-semibold">matplotlib</span>
|
||||||
|
<span className="text-gray-500 ml-2">3.10.1</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<span className="text-yellow-600 font-semibold">scipy</span>
|
||||||
|
<span className="text-gray-500 ml-2">1.15.2</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<span className="text-yellow-600 font-semibold">scikit-learn</span>
|
||||||
|
<span className="text-gray-500 ml-2">1.6.1</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<span className="text-yellow-600 font-semibold">seaborn</span>
|
||||||
|
<span className="text-gray-500 ml-2">0.13.2</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<span className="text-yellow-600 font-semibold">statsmodels</span>
|
||||||
|
<span className="text-gray-500 ml-2">0.14.4</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={copyToClipboard}
|
||||||
|
className="flex items-center text-sm text-gray-500 hover:text-gray-700 transition-colors"
|
||||||
|
title="Копировать код"
|
||||||
|
>
|
||||||
|
{copied ? (
|
||||||
|
<Check className="w-4 h-4" />
|
||||||
|
) : (
|
||||||
|
<Copy className="w-4 h-4" />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="p-4">
|
<div className="p-4">
|
||||||
|
|||||||
@@ -155,10 +155,10 @@ const TaskSolution: React.FC<TaskSolutionProps> = ({
|
|||||||
? 'bg-blue-50 text-blue-700'
|
? 'bg-blue-50 text-blue-700'
|
||||||
: 'bg-red-50 text-red-700'}`}
|
: 'bg-red-50 text-red-700'}`}
|
||||||
>
|
>
|
||||||
{hasSubmissionsLeft ? (
|
{maxAttempts === -1 || hasSubmissionsLeft ? (
|
||||||
<>
|
<>
|
||||||
<span className="font-medium">
|
<span className="font-medium">
|
||||||
Осталось посылок: {submissionsLeft === Infinity ? '∞' : submissionsLeft}
|
Осталось посылок: {maxAttempts === -1 ? '∞' : submissionsLeft}
|
||||||
</span>
|
</span>
|
||||||
{maxAttempts !== -1 && (
|
{maxAttempts !== -1 && (
|
||||||
<span className="text-blue-500 ml-1">
|
<span className="text-blue-500 ml-1">
|
||||||
|
|||||||
Reference in New Issue
Block a user