mirror of
https://gitlab.com/megazordpobeda/DataRush.git
synced 2026-05-23 02:47:10 +00:00
Merge branch 'master' of gitlab.prodcontest.ru:team-15/project
This commit is contained in:
@@ -0,0 +1 @@
|
|||||||
|
.idea
|
||||||
@@ -4,8 +4,8 @@ from uuid import UUID
|
|||||||
from django.http import HttpRequest
|
from django.http import HttpRequest
|
||||||
from ninja import ModelSchema, Schema
|
from ninja import ModelSchema, Schema
|
||||||
|
|
||||||
from apps.review.models import Reviewer
|
from apps.review.models import Reviewer, Review
|
||||||
from apps.task.models import CompetetionTaskSumbission
|
from apps.task.models import CompetitionTaskSubmission
|
||||||
|
|
||||||
|
|
||||||
class PingOut(Schema):
|
class PingOut(Schema):
|
||||||
@@ -25,13 +25,13 @@ class SubmissionOut(ModelSchema):
|
|||||||
status: Literal["sent", "checking", "checked"]
|
status: Literal["sent", "checking", "checked"]
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = CompetetionTaskSumbission
|
model = CompetitionTaskSubmission
|
||||||
exclude = ("user",)
|
exclude = ("user",)
|
||||||
|
|
||||||
|
|
||||||
class SubmissionsOut(Schema):
|
class SubmissionsOut(Schema):
|
||||||
submissions: list[SubmissionOut] = []
|
submissions: list = None
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def resolve_submissions(self, context: HttpRequest) -> list[SubmissionOut]:
|
def resolve_submissions(self, context) -> list[SubmissionOut]:
|
||||||
return list(CompetetionTaskSumbission.objects.all())
|
return list(Review.objects.filter(reviewer=context.get("request").auth))
|
||||||
@@ -1,10 +1,15 @@
|
|||||||
|
import logging
|
||||||
from http import HTTPStatus as status
|
from http import HTTPStatus as status
|
||||||
|
from uuid import UUID
|
||||||
|
|
||||||
from django.http import HttpRequest
|
from django.http import HttpRequest
|
||||||
|
from django.shortcuts import get_object_or_404
|
||||||
from ninja import Router
|
from ninja import Router
|
||||||
|
|
||||||
from api.v1 import schemas as global_schemas
|
from api.v1 import schemas as global_schemas
|
||||||
from api.v1.review import schemas
|
from api.v1.review import schemas
|
||||||
|
from api.v1.task.schemas import TaskSubmissionIn
|
||||||
|
from apps.task.models import CompetitionTaskSubmission
|
||||||
|
|
||||||
router = Router(tags=["review"])
|
router = Router(tags=["review"])
|
||||||
|
|
||||||
@@ -16,7 +21,7 @@ router = Router(tags=["review"])
|
|||||||
},
|
},
|
||||||
description="Список отправок, на проверку которых назначен ревьюер"
|
description="Список отправок, на проверку которых назначен ревьюер"
|
||||||
)
|
)
|
||||||
def get_submissions(request: HttpRequest, token) -> tuple[status, schemas.SubmissionsOut]:
|
def get_submissions(request: HttpRequest, token: str) -> tuple[status, schemas.SubmissionsOut]:
|
||||||
return status.OK, schemas.SubmissionsOut()
|
return status.OK, schemas.SubmissionsOut()
|
||||||
|
|
||||||
|
|
||||||
@@ -30,3 +35,13 @@ def get_submissions(request: HttpRequest, token) -> tuple[status, schemas.Submis
|
|||||||
)
|
)
|
||||||
def get_reviewer_profile(request: HttpRequest, token: str):
|
def get_reviewer_profile(request: HttpRequest, token: str):
|
||||||
return status.OK, request.auth
|
return status.OK, request.auth
|
||||||
|
|
||||||
|
@router.get(
|
||||||
|
"{token}/submissions/{submition_id}",
|
||||||
|
response={
|
||||||
|
status.OK: schemas.SubmissionOut,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
def get_submission(request: HttpRequest, token: str, submition_id: UUID) -> tuple[status, schemas.SubmissionsOut]:
|
||||||
|
submission = get_object_or_404(CompetitionTaskSubmission, id=submition_id)
|
||||||
|
return status.OK, submission
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ from api.v1.task.schemas import (
|
|||||||
)
|
)
|
||||||
from apps.competition.models import State
|
from apps.competition.models import State
|
||||||
from apps.task.models import (
|
from apps.task.models import (
|
||||||
CompetetionTaskSumbission,
|
CompetitionTaskSubmission,
|
||||||
Competition,
|
Competition,
|
||||||
CompetitionTask,
|
CompetitionTask,
|
||||||
)
|
)
|
||||||
@@ -96,23 +96,23 @@ def submit_task(
|
|||||||
)
|
)
|
||||||
|
|
||||||
if task.type == CompetitionTask.CompetitionTaskType.INPUT:
|
if task.type == CompetitionTask.CompetitionTaskType.INPUT:
|
||||||
CompetetionTaskSumbission.objects.create(
|
CompetitionTaskSubmission.objects.create(
|
||||||
user=user,
|
user=user,
|
||||||
task=task,
|
task=task,
|
||||||
status=CompetetionTaskSumbission.StatusChoices.CHECKED,
|
status=CompetitionTaskSubmission.StatusChoices.CHECKED,
|
||||||
result={"correct": submission.content == task.answer_file_path},
|
result={"correct": submission.content == task.answer_file_path},
|
||||||
)
|
)
|
||||||
if task.type == CompetitionTask.CompetitionTaskType.REVIEW:
|
if task.type == CompetitionTask.CompetitionTaskType.REVIEW:
|
||||||
CompetetionTaskSumbission.objects.create(
|
CompetitionTaskSubmission.objects.create(
|
||||||
user=user,
|
user=user,
|
||||||
task=task,
|
task=task,
|
||||||
status=CompetetionTaskSumbission.StatusChoices.SENT,
|
status=CompetitionTaskSubmission.StatusChoices.SENT,
|
||||||
)
|
)
|
||||||
if task.type == CompetitionTask.CompetitionTaskType.CHECKER:
|
if task.type == CompetitionTask.CompetitionTaskType.CHECKER:
|
||||||
CompetetionTaskSumbission.objects.create(
|
CompetitionTaskSubmission.objects.create(
|
||||||
user=user,
|
user=user,
|
||||||
task=task,
|
task=task,
|
||||||
status=CompetetionTaskSumbission.StatusChoices.CHECKING,
|
status=CompetitionTaskSubmission.StatusChoices.CHECKING,
|
||||||
)
|
)
|
||||||
|
|
||||||
return TaskSubmissionOut(id=CompetetionTaskSumbission.id)
|
return TaskSubmissionOut(id=CompetitionTaskSubmission.id)
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ def sign_in(request, data: LoginSchema):
|
|||||||
user = User.objects.filter(email=data.email).first()
|
user = User.objects.filter(email=data.email).first()
|
||||||
if not user:
|
if not user:
|
||||||
raise AuthenticationError
|
raise AuthenticationError
|
||||||
if user.password != data.password:
|
if not user.check_password(data.password):
|
||||||
raise AuthenticationError
|
raise AuthenticationError
|
||||||
|
|
||||||
token = BearerAuth.generate_jwt(user)
|
token = BearerAuth.generate_jwt(user)
|
||||||
|
|||||||
+49
@@ -0,0 +1,49 @@
|
|||||||
|
# Generated by Django 5.1.6 on 2025-03-01 14:46
|
||||||
|
|
||||||
|
import tinymce.models
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('competition', '0003_remove_competition_tasks'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='competition',
|
||||||
|
name='description',
|
||||||
|
field=tinymce.models.HTMLField(verbose_name='описание'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='competition',
|
||||||
|
name='end_date',
|
||||||
|
field=models.DateTimeField(blank=True, null=True, verbose_name='дедлайн участия'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='competition',
|
||||||
|
name='image_url',
|
||||||
|
field=models.FileField(blank=True, null=True, upload_to='', verbose_name='изображение соревнования'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='competition',
|
||||||
|
name='participation_type',
|
||||||
|
field=models.CharField(choices=[('edu', 'Образовательный'), ('competitive', 'Соревновательный')], max_length=11, verbose_name='тип соревнования'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='competition',
|
||||||
|
name='start_date',
|
||||||
|
field=models.DateTimeField(blank=True, null=True, verbose_name='дедлайн участия'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='competition',
|
||||||
|
name='title',
|
||||||
|
field=models.CharField(max_length=100, verbose_name='аазвание'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='competition',
|
||||||
|
name='type',
|
||||||
|
field=models.CharField(choices=[('solo', 'Индивидуальный')], max_length=10, verbose_name='тип участия'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
from tinymce.models import HTMLField
|
||||||
|
|
||||||
from apps.core.models import BaseModel
|
from apps.core.models import BaseModel
|
||||||
from apps.user.models import User
|
from apps.user.models import User
|
||||||
@@ -17,29 +18,29 @@ class Competition(BaseModel):
|
|||||||
def image_url_upload_to(instance, filename):
|
def image_url_upload_to(instance, filename):
|
||||||
return f"/competitions/{instance.id}/image"
|
return f"/competitions/{instance.id}/image"
|
||||||
|
|
||||||
title = models.CharField(max_length=100, verbose_name="Название")
|
title = models.CharField(max_length=100, verbose_name="название")
|
||||||
description = models.TextField(verbose_name="Описание")
|
description = models.TextField(verbose_name="описание")
|
||||||
image_url = models.FileField(
|
image_url = models.FileField(
|
||||||
verbose_name="Изображение соревнования",
|
verbose_name="изображение соревнования",
|
||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
upload_to=image_url_upload_to,
|
upload_to=image_url_upload_to,
|
||||||
)
|
)
|
||||||
end_date = models.DateTimeField(
|
end_date = models.DateTimeField(
|
||||||
verbose_name="Дедлайн участия", null=True, blank=True
|
verbose_name="дедлайн участия", null=True, blank=True
|
||||||
)
|
)
|
||||||
start_date = models.DateTimeField(
|
start_date = models.DateTimeField(
|
||||||
verbose_name="Дедлайн участия", null=True, blank=True
|
verbose_name="дедлайн участия", null=True, blank=True
|
||||||
)
|
)
|
||||||
type = models.CharField(
|
type = models.CharField(
|
||||||
max_length=10,
|
max_length=10,
|
||||||
choices=CompetitionType.choices,
|
choices=CompetitionType.choices,
|
||||||
verbose_name="Тип участия",
|
verbose_name="тип участия",
|
||||||
)
|
)
|
||||||
participation_type = models.CharField(
|
participation_type = models.CharField(
|
||||||
max_length=11,
|
max_length=11,
|
||||||
choices=CompetitionParticipationType.choices,
|
choices=CompetitionParticipationType.choices,
|
||||||
verbose_name="Тип соревнования",
|
verbose_name="тип соревнования",
|
||||||
)
|
)
|
||||||
participants = models.ManyToManyField(
|
participants = models.ManyToManyField(
|
||||||
User, related_name="participants", blank=True, editable=False
|
User, related_name="participants", blank=True, editable=False
|
||||||
@@ -48,6 +49,8 @@ class Competition(BaseModel):
|
|||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.title
|
return self.title
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = "соревнование"
|
verbose_name = "соревнование"
|
||||||
verbose_name_plural = "соревнования"
|
verbose_name_plural = "соревнования"
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ from django.core.management.base import BaseCommand
|
|||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
|
||||||
from apps.competition.models import Competition, State
|
from apps.competition.models import Competition, State
|
||||||
from apps.task.models import CompetetionTaskSumbission, CompetitionTask
|
from apps.task.models import CompetitionTaskSubmission, CompetitionTask
|
||||||
from apps.user.models import User, UserRole
|
from apps.user.models import User, UserRole
|
||||||
|
|
||||||
|
|
||||||
@@ -105,7 +105,7 @@ class Command(BaseCommand):
|
|||||||
b"Submission content",
|
b"Submission content",
|
||||||
name=f"submission_{uuid.uuid4().hex}.txt",
|
name=f"submission_{uuid.uuid4().hex}.txt",
|
||||||
)
|
)
|
||||||
submission = CompetetionTaskSumbission.objects.create(
|
submission = CompetitionTaskSubmission.objects.create(
|
||||||
user=user,
|
user=user,
|
||||||
task=task,
|
task=task,
|
||||||
earned_points=random.randint(
|
earned_points=random.randint(
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
# Generated by Django 5.1.6 on 2025-03-01 14:47
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
import uuid
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('review', '0001_initial'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Review',
|
||||||
|
fields=[
|
||||||
|
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
||||||
|
('state', models.CharField(choices=[('not_checked', 'Not Checked'), ('checking', 'Checking'), ('checked', 'Checked')], max_length=11)),
|
||||||
|
('reviewer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='review.reviewer')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'abstract': False,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
# Generated by Django 5.1.6 on 2025-03-01 14:47
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('review', '0002_review'),
|
||||||
|
('task', '0005_alter_competitiontask_description_and_more'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='review',
|
||||||
|
name='submission',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='task.competitiontasksubmission'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
from django.db import models
|
from django.db import models
|
||||||
|
|
||||||
from apps.core.models import BaseModel
|
from apps.core.models import BaseModel
|
||||||
|
from apps.task.models import CompetitionTaskSubmission
|
||||||
|
|
||||||
|
|
||||||
class Reviewer(BaseModel):
|
class Reviewer(BaseModel):
|
||||||
@@ -8,3 +9,14 @@ class Reviewer(BaseModel):
|
|||||||
surname = models.CharField(max_length=100)
|
surname = models.CharField(max_length=100)
|
||||||
|
|
||||||
token = models.CharField(max_length=100)
|
token = models.CharField(max_length=100)
|
||||||
|
|
||||||
|
class Review(BaseModel):
|
||||||
|
class ReviewStatusChoices(models.TextChoices):
|
||||||
|
NOT_CHECKED = "not_checked"
|
||||||
|
CHECKING = "checking"
|
||||||
|
CHECKED = "checked"
|
||||||
|
|
||||||
|
reviewer = models.ForeignKey(Reviewer, on_delete=models.CASCADE)
|
||||||
|
submission = models.ForeignKey(CompetitionTaskSubmission, on_delete=models.CASCADE)
|
||||||
|
|
||||||
|
state = models.CharField(choices=ReviewStatusChoices.choices, max_length=11)
|
||||||
@@ -38,8 +38,8 @@ class Migration(migrations.Migration):
|
|||||||
fields=[
|
fields=[
|
||||||
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
||||||
('status', models.CharField(choices=[('sent', 'Sent'), ('checking', 'Checking'), ('checked', 'Checked')], default='sent', max_length=8)),
|
('status', models.CharField(choices=[('sent', 'Sent'), ('checking', 'Checking'), ('checked', 'Checked')], default='sent', max_length=8)),
|
||||||
('content', models.FileField(upload_to=apps.task.models.CompetetionTaskSumbission.submission_content_upload_to)),
|
('content', models.FileField(upload_to=apps.task.models.CompetitionTaskSubmission.submission_content_upload_to)),
|
||||||
('stdout', models.FileField(blank=True, null=True, upload_to=apps.task.models.CompetetionTaskSumbission.submission_stdout_upload_to)),
|
('stdout', models.FileField(blank=True, null=True, upload_to=apps.task.models.CompetitionTaskSubmission.submission_stdout_upload_to)),
|
||||||
('result', models.JSONField(blank=True, default=None, null=True)),
|
('result', models.JSONField(blank=True, default=None, null=True)),
|
||||||
('earned_points', models.IntegerField()),
|
('earned_points', models.IntegerField()),
|
||||||
('timestamp', models.DateTimeField(auto_now_add=True)),
|
('timestamp', models.DateTimeField(auto_now_add=True)),
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
# Generated by Django 5.1.6 on 2025-03-01 12:54
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('review', '0001_initial'),
|
||||||
|
('task', '0001_initial'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='competetiontasksumbission',
|
||||||
|
name='reviewers',
|
||||||
|
field=models.ManyToManyField(blank=True, related_name='reviewers', to='review.reviewer'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
# Generated by Django 5.1.6 on 2025-03-01 14:39
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('task', '0002_competetiontasksumbission_reviewers'),
|
||||||
|
('task', '0003_competitiontask_max_attemps_and_more'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
]
|
||||||
+48
@@ -0,0 +1,48 @@
|
|||||||
|
# Generated by Django 5.1.6 on 2025-03-01 14:47
|
||||||
|
|
||||||
|
import apps.task.models
|
||||||
|
import django.db.models.deletion
|
||||||
|
import tinymce.models
|
||||||
|
import uuid
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('task', '0004_merge_20250301_1739'),
|
||||||
|
('user', '0002_alter_user_email_alter_user_password_and_more'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='competitiontask',
|
||||||
|
name='description',
|
||||||
|
field=tinymce.models.HTMLField(max_length=300, verbose_name='описание'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='competitiontask',
|
||||||
|
name='max_attemps',
|
||||||
|
field=models.PositiveSmallIntegerField(),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='CompetitionTaskSubmission',
|
||||||
|
fields=[
|
||||||
|
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
||||||
|
('status', models.CharField(choices=[('sent', 'Sent'), ('checking', 'Checking'), ('checked', 'Checked')], default='sent', max_length=8)),
|
||||||
|
('content', models.FileField(upload_to=apps.task.models.CompetitionTaskSubmission.submission_content_upload_to)),
|
||||||
|
('stdout', models.FileField(blank=True, null=True, upload_to=apps.task.models.CompetitionTaskSubmission.submission_stdout_upload_to)),
|
||||||
|
('result', models.JSONField(blank=True, default=None, null=True)),
|
||||||
|
('earned_points', models.IntegerField()),
|
||||||
|
('timestamp', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('task', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='task.competitiontask')),
|
||||||
|
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='user.user')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'abstract': False,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.DeleteModel(
|
||||||
|
name='CompetetionTaskSumbission',
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
from tinymce.models import HTMLField
|
||||||
|
|
||||||
from apps.competition.models import Competition
|
from apps.competition.models import Competition
|
||||||
from apps.core.models import BaseModel
|
from apps.core.models import BaseModel
|
||||||
@@ -19,8 +20,8 @@ class CompetitionTask(BaseModel):
|
|||||||
|
|
||||||
competition = models.ForeignKey(Competition, on_delete=models.CASCADE)
|
competition = models.ForeignKey(Competition, on_delete=models.CASCADE)
|
||||||
title = models.CharField(verbose_name="заголовок", max_length=50)
|
title = models.CharField(verbose_name="заголовок", max_length=50)
|
||||||
description = models.TextField(verbose_name="описание", max_length=300)
|
description = HTMLField(verbose_name="описание", max_length=300)
|
||||||
max_attemps = models.PositiveSmallIntegerField(default=0)
|
max_attemps = models.PositiveSmallIntegerField()
|
||||||
type = models.CharField(
|
type = models.CharField(
|
||||||
choices=CompetitionTaskType, max_length=8, verbose_name="тип проверки"
|
choices=CompetitionTaskType, max_length=8, verbose_name="тип проверки"
|
||||||
)
|
)
|
||||||
@@ -63,7 +64,7 @@ class CompetitionTask(BaseModel):
|
|||||||
verbose_name_plural = "задания"
|
verbose_name_plural = "задания"
|
||||||
|
|
||||||
|
|
||||||
class CompetetionTaskSumbission(BaseModel):
|
class CompetitionTaskSubmission(BaseModel):
|
||||||
class StatusChoices(models.TextChoices):
|
class StatusChoices(models.TextChoices):
|
||||||
SENT = "sent"
|
SENT = "sent"
|
||||||
CHECKING = "checking"
|
CHECKING = "checking"
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
from django.contrib import admin
|
||||||
|
|
||||||
|
from apps.user.models import User
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(User)
|
||||||
|
class UserAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ("email", "username")
|
||||||
|
search_fields = ("id", "email", "username")
|
||||||
@@ -5,3 +5,4 @@ class UsersConfig(AppConfig):
|
|||||||
default_auto_field = "django.db.models.BigAutoField"
|
default_auto_field = "django.db.models.BigAutoField"
|
||||||
name = "apps.user"
|
name = "apps.user"
|
||||||
label = "user"
|
label = "user"
|
||||||
|
verbose_name = "Пользователи"
|
||||||
|
|||||||
+28
@@ -0,0 +1,28 @@
|
|||||||
|
# Generated by Django 5.1.6 on 2025-03-01 14:46
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('user', '0001_initial'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='user',
|
||||||
|
name='email',
|
||||||
|
field=models.EmailField(max_length=254, unique=True, verbose_name='почта'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='user',
|
||||||
|
name='password',
|
||||||
|
field=models.TextField(verbose_name='пароль'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='user',
|
||||||
|
name='username',
|
||||||
|
field=models.SlugField(unique=True, verbose_name='юзернейм'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
from django.db import models
|
from django.db import models
|
||||||
|
from django.contrib.auth.hashers import check_password, make_password
|
||||||
|
|
||||||
from apps.core.models import BaseModel
|
from apps.core.models import BaseModel
|
||||||
|
|
||||||
@@ -9,9 +10,15 @@ class UserRole(models.Choices):
|
|||||||
|
|
||||||
|
|
||||||
class User(BaseModel):
|
class User(BaseModel):
|
||||||
email = models.EmailField(unique=True, verbose_name="Почта")
|
email = models.EmailField(unique=True, verbose_name="почта")
|
||||||
username = models.SlugField(unique=True, verbose_name="Юзернейм")
|
username = models.SlugField(unique=True, verbose_name="юзернейм")
|
||||||
password = models.TextField(verbose_name="Пароль")
|
password = models.TextField(verbose_name="пароль")
|
||||||
|
|
||||||
|
def make_password(self):
|
||||||
|
return make_password(self.password)
|
||||||
|
|
||||||
|
def check_password(self, password):
|
||||||
|
return check_password(self.password, password)
|
||||||
|
|
||||||
status = models.CharField(
|
status = models.CharField(
|
||||||
max_length=10, choices=UserRole, default="student"
|
max_length=10, choices=UserRole, default="student"
|
||||||
|
|||||||
@@ -441,6 +441,7 @@ INSTALLED_APPS = [
|
|||||||
"django_guid",
|
"django_guid",
|
||||||
"ninja",
|
"ninja",
|
||||||
"minio_storage",
|
"minio_storage",
|
||||||
|
"tinymce",
|
||||||
# Internal apps
|
# Internal apps
|
||||||
"apps.core",
|
"apps.core",
|
||||||
"apps.user",
|
"apps.user",
|
||||||
@@ -449,6 +450,22 @@ INSTALLED_APPS = [
|
|||||||
"apps.task",
|
"apps.task",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
# tinymce
|
||||||
|
TINYMCE_DEFAULT_CONFIG = {
|
||||||
|
"theme": "silver",
|
||||||
|
"height": 500,
|
||||||
|
"menubar": False,
|
||||||
|
"plugins": "advlist,autolink,lists,link,image,charmap,print,preview,anchor,"
|
||||||
|
"searchreplace,visualblocks,code,fullscreen,insertdatetime,media,table,paste,"
|
||||||
|
"code,help,wordcount",
|
||||||
|
"toolbar": "undo redo | formatselect | "
|
||||||
|
"bold italic backcolor | alignleft aligncenter "
|
||||||
|
"alignright alignjustify | bullist numlist outdent indent | "
|
||||||
|
"removeformat | help",
|
||||||
|
"skin": "oxide-dark",
|
||||||
|
"content_css": "dark"
|
||||||
|
}
|
||||||
|
|
||||||
# GUID
|
# GUID
|
||||||
|
|
||||||
DJANGO_GUID = {
|
DJANGO_GUID = {
|
||||||
@@ -466,6 +483,10 @@ DJANGO_GUID = {
|
|||||||
|
|
||||||
LANGUAGE_COOKIE_AGE = 31449600
|
LANGUAGE_COOKIE_AGE = 31449600
|
||||||
|
|
||||||
|
PASSWORD_HASHERS = [
|
||||||
|
"django.contrib.auth.hashers.Argon2PasswordHasher",
|
||||||
|
]
|
||||||
|
|
||||||
LANGUAGE_COOKIE_DOMAIN = None
|
LANGUAGE_COOKIE_DOMAIN = None
|
||||||
|
|
||||||
LANGUAGE_COOKIE_HTTPONLY = False
|
LANGUAGE_COOKIE_HTTPONLY = False
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ admin.site.index_title = "DataRush"
|
|||||||
|
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
|
# tinymce
|
||||||
|
path('tinymce/', include('tinymce.urls')),
|
||||||
# Admin urls
|
# Admin urls
|
||||||
path("admin/", admin.site.urls),
|
path("admin/", admin.site.urls),
|
||||||
# API urls
|
# API urls
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ version = "0.1.0"
|
|||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.10,<3.12"
|
requires-python = ">=3.10,<3.12"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"argon2-cffi>=23.1.0",
|
||||||
"celery>=5.4.0",
|
"celery>=5.4.0",
|
||||||
"colorlog>=6.9.0",
|
"colorlog>=6.9.0",
|
||||||
"django-cors-headers>=4.6.0",
|
"django-cors-headers>=4.6.0",
|
||||||
@@ -13,7 +14,9 @@ dependencies = [
|
|||||||
"django-health-check>=3.18.3",
|
"django-health-check>=3.18.3",
|
||||||
"django-minio-storage>=0.5.7",
|
"django-minio-storage>=0.5.7",
|
||||||
"django-ninja>=1.3.0",
|
"django-ninja>=1.3.0",
|
||||||
|
"django-pagedown>=2.2.1",
|
||||||
"django-stubs-ext>=5.1.3",
|
"django-stubs-ext>=5.1.3",
|
||||||
|
"django-tinymce>=4.1.0",
|
||||||
"gunicorn>=23.0.0",
|
"gunicorn>=23.0.0",
|
||||||
"httpx>=0.28.1",
|
"httpx>=0.28.1",
|
||||||
"pillow>=11.1.0",
|
"pillow>=11.1.0",
|
||||||
|
|||||||
@@ -0,0 +1,130 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
import { Drawer as DrawerPrimitive } from "vaul"
|
||||||
|
|
||||||
|
import { cn } from "@/shared/lib/utils"
|
||||||
|
|
||||||
|
function Drawer({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DrawerPrimitive.Root>) {
|
||||||
|
return <DrawerPrimitive.Root data-slot="drawer" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function DrawerTrigger({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DrawerPrimitive.Trigger>) {
|
||||||
|
return <DrawerPrimitive.Trigger data-slot="drawer-trigger" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function DrawerPortal({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DrawerPrimitive.Portal>) {
|
||||||
|
return <DrawerPrimitive.Portal data-slot="drawer-portal" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function DrawerClose({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DrawerPrimitive.Close>) {
|
||||||
|
return <DrawerPrimitive.Close data-slot="drawer-close" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function DrawerOverlay({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DrawerPrimitive.Overlay>) {
|
||||||
|
return (
|
||||||
|
<DrawerPrimitive.Overlay
|
||||||
|
data-slot="drawer-overlay"
|
||||||
|
className={cn(
|
||||||
|
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/80",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DrawerContent({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DrawerPrimitive.Content>) {
|
||||||
|
return (
|
||||||
|
<DrawerPortal data-slot="drawer-portal">
|
||||||
|
<DrawerOverlay />
|
||||||
|
<DrawerPrimitive.Content
|
||||||
|
data-slot="drawer-content"
|
||||||
|
className={cn(
|
||||||
|
"group/drawer-content bg-background fixed z-50 flex h-auto flex-col",
|
||||||
|
"data-[vaul-drawer-direction=top]:inset-x-0 data-[vaul-drawer-direction=top]:top-0 data-[vaul-drawer-direction=top]:mb-24 data-[vaul-drawer-direction=top]:max-h-[80vh] data-[vaul-drawer-direction=top]:rounded-b-lg",
|
||||||
|
"data-[vaul-drawer-direction=bottom]:inset-x-0 data-[vaul-drawer-direction=bottom]:bottom-0 data-[vaul-drawer-direction=bottom]:mt-24 data-[vaul-drawer-direction=bottom]:max-h-[80vh] data-[vaul-drawer-direction=bottom]:rounded-t-lg",
|
||||||
|
"data-[vaul-drawer-direction=right]:inset-y-0 data-[vaul-drawer-direction=right]:right-0 data-[vaul-drawer-direction=right]:w-3/4 data-[vaul-drawer-direction=right]:sm:max-w-sm",
|
||||||
|
"data-[vaul-drawer-direction=left]:inset-y-0 data-[vaul-drawer-direction=left]:left-0 data-[vaul-drawer-direction=left]:w-3/4 data-[vaul-drawer-direction=left]:sm:max-w-sm",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<div className="bg-muted mx-auto mt-4 hidden h-2 w-[100px] shrink-0 rounded-full group-data-[vaul-drawer-direction=bottom]/drawer-content:block" />
|
||||||
|
{children}
|
||||||
|
</DrawerPrimitive.Content>
|
||||||
|
</DrawerPortal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DrawerHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="drawer-header"
|
||||||
|
className={cn("flex flex-col gap-1.5 p-4", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DrawerFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="drawer-footer"
|
||||||
|
className={cn("mt-auto flex flex-col gap-2 p-4", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DrawerTitle({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DrawerPrimitive.Title>) {
|
||||||
|
return (
|
||||||
|
<DrawerPrimitive.Title
|
||||||
|
data-slot="drawer-title"
|
||||||
|
className={cn("text-foreground font-semibold", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DrawerDescription({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DrawerPrimitive.Description>) {
|
||||||
|
return (
|
||||||
|
<DrawerPrimitive.Description
|
||||||
|
data-slot="drawer-description"
|
||||||
|
className={cn("text-muted-foreground text-sm", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
Drawer,
|
||||||
|
DrawerPortal,
|
||||||
|
DrawerOverlay,
|
||||||
|
DrawerTrigger,
|
||||||
|
DrawerClose,
|
||||||
|
DrawerContent,
|
||||||
|
DrawerHeader,
|
||||||
|
DrawerFooter,
|
||||||
|
DrawerTitle,
|
||||||
|
DrawerDescription,
|
||||||
|
}
|
||||||
@@ -0,0 +1,134 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
import * as SheetPrimitive from "@radix-ui/react-dialog"
|
||||||
|
import { XIcon } from "lucide-react"
|
||||||
|
|
||||||
|
import { cn } from "@/shared/lib/utils"
|
||||||
|
|
||||||
|
function Sheet({ ...props }: React.ComponentProps<typeof SheetPrimitive.Root>) {
|
||||||
|
return <SheetPrimitive.Root data-slot="sheet" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function SheetTrigger({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SheetPrimitive.Trigger>) {
|
||||||
|
return <SheetPrimitive.Trigger data-slot="sheet-trigger" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function SheetClose({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SheetPrimitive.Close>) {
|
||||||
|
return <SheetPrimitive.Close data-slot="sheet-close" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function SheetPortal({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SheetPrimitive.Portal>) {
|
||||||
|
return <SheetPrimitive.Portal data-slot="sheet-portal" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function SheetOverlay({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SheetPrimitive.Overlay>) {
|
||||||
|
return (
|
||||||
|
<SheetPrimitive.Overlay
|
||||||
|
data-slot="sheet-overlay"
|
||||||
|
className={cn(
|
||||||
|
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/80",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function SheetContent({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
side = "right",
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SheetPrimitive.Content> & {
|
||||||
|
side?: "top" | "right" | "bottom" | "left"
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<SheetPortal>
|
||||||
|
<SheetOverlay />
|
||||||
|
<SheetPrimitive.Content
|
||||||
|
data-slot="sheet-content"
|
||||||
|
className={cn(
|
||||||
|
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out fixed z-50 flex flex-col gap-4 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500",
|
||||||
|
side === "right" &&
|
||||||
|
"data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right inset-y-0 right-0 h-full w-3/4 border-l sm:max-w-sm",
|
||||||
|
side === "left" &&
|
||||||
|
"data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left inset-y-0 left-0 h-full w-3/4 border-r sm:max-w-sm",
|
||||||
|
side === "top" &&
|
||||||
|
"data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top inset-x-0 top-0 h-auto border-b",
|
||||||
|
side === "bottom" &&
|
||||||
|
"data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom inset-x-0 bottom-0 h-auto border-t",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
{/* Removed the default close button that was causing duplication */}
|
||||||
|
</SheetPrimitive.Content>
|
||||||
|
</SheetPortal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function SheetHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="sheet-header"
|
||||||
|
className={cn("flex flex-col gap-1.5 p-4", className)} // Kept original padding
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function SheetFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="sheet-footer"
|
||||||
|
className={cn("mt-auto flex flex-col gap-2 p-4", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function SheetTitle({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SheetPrimitive.Title>) {
|
||||||
|
return (
|
||||||
|
<SheetPrimitive.Title
|
||||||
|
data-slot="sheet-title"
|
||||||
|
className={cn("text-foreground font-semibold", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function SheetDescription({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SheetPrimitive.Description>) {
|
||||||
|
return (
|
||||||
|
<SheetPrimitive.Description
|
||||||
|
data-slot="sheet-description"
|
||||||
|
className={cn("text-muted-foreground text-sm", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
Sheet,
|
||||||
|
SheetTrigger,
|
||||||
|
SheetClose,
|
||||||
|
SheetContent,
|
||||||
|
SheetHeader,
|
||||||
|
SheetFooter,
|
||||||
|
SheetTitle,
|
||||||
|
SheetDescription,
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useParams, Navigate } from "react-router-dom";
|
import { useParams, Navigate } from "react-router-dom";
|
||||||
import { Task } from "@/shared/types";
|
import { Task } from "@/shared/types";
|
||||||
import { mockTasks } from "@/shared/mocks/mocks";
|
import { mockSolutions, mockTasks } from "@/shared/mocks/mocks";
|
||||||
import CompetitionHeader from "./components/CompetitionHeader";
|
import CompetitionHeader from "./components/CompetitionHeader";
|
||||||
import TaskContent from "./components/TaskContent";
|
import TaskContent from "./components/TaskContent";
|
||||||
import TaskSolution from "./modules/TaskSolution";
|
import TaskSolution from "./modules/TaskSolution";
|
||||||
@@ -40,6 +40,7 @@ const CompetitionSessionPage = () => {
|
|||||||
<TaskContent task={currentTask} />
|
<TaskContent task={currentTask} />
|
||||||
<TaskSolution
|
<TaskSolution
|
||||||
task={currentTask}
|
task={currentTask}
|
||||||
|
solutions={mockSolutions}
|
||||||
answer={answer}
|
answer={answer}
|
||||||
setAnswer={setAnswer}
|
setAnswer={setAnswer}
|
||||||
onSubmit={handleSubmit}
|
onSubmit={handleSubmit}
|
||||||
|
|||||||
+28
-5
@@ -1,28 +1,51 @@
|
|||||||
import React from 'react';
|
import React, { useState } from 'react';
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
|
import SolutionHistorySheet from '../SolutionHistorySheet';
|
||||||
|
import { Solution } from "@/shared/types";
|
||||||
|
import { mockSolutions } from '@/shared/mocks/mocks';
|
||||||
|
|
||||||
interface ActionButtonsProps {
|
interface ActionButtonsProps {
|
||||||
onHistoryClick: () => void;
|
onHistoryClick: () => void;
|
||||||
onSubmit: () => void;
|
onSubmit: () => void;
|
||||||
|
solutionHistory?: Solution[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const ActionButtons: React.FC<ActionButtonsProps> = ({ onHistoryClick, onSubmit }) => {
|
const ActionButtons: React.FC<ActionButtonsProps> = ({
|
||||||
|
onHistoryClick,
|
||||||
|
onSubmit,
|
||||||
|
solutionHistory = mockSolutions
|
||||||
|
}) => {
|
||||||
|
const [isHistoryOpen, setIsHistoryOpen] = useState(false);
|
||||||
|
|
||||||
|
const handleHistoryClick = () => {
|
||||||
|
setIsHistoryOpen(true);
|
||||||
|
onHistoryClick();
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex gap-3 justify-between">
|
<>
|
||||||
|
<div className="flex gap-8">
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
className="font-hse-sans bg-white hover:bg-gray-100"
|
className="font-hse-sans bg-white hover:bg-gray-100"
|
||||||
onClick={onHistoryClick}
|
onClick={handleHistoryClick}
|
||||||
>
|
>
|
||||||
История
|
История
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
onClick={onSubmit}
|
onClick={onSubmit}
|
||||||
className="font-hse-sans"
|
className="font-hse-sans flex-grow"
|
||||||
>
|
>
|
||||||
Отправить решение
|
Отправить решение
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
{/* чуть-чуть рак */}
|
||||||
|
<SolutionHistorySheet
|
||||||
|
isOpen={isHistoryOpen}
|
||||||
|
onOpenChange={setIsHistoryOpen}
|
||||||
|
solutions={solutionHistory}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
+51
@@ -0,0 +1,51 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Sheet, SheetClose, SheetContent, SheetHeader, SheetTitle } from "@/components/ui/sheet";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { X } from "lucide-react";
|
||||||
|
import SolutionStatus from '../SolutionStatus';
|
||||||
|
import { Solution } from "@/shared/types";
|
||||||
|
|
||||||
|
interface SolutionHistorySheetProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
onOpenChange: (open: boolean) => void;
|
||||||
|
solutions: Solution[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const SolutionHistorySheet: React.FC<SolutionHistorySheetProps> = ({
|
||||||
|
isOpen,
|
||||||
|
onOpenChange,
|
||||||
|
solutions
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<Sheet open={isOpen} onOpenChange={onOpenChange}>
|
||||||
|
<SheetContent className="w-[350px] sm:w-[450px] p-0">
|
||||||
|
<SheetHeader className="border-b py-3 px-4">
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<SheetTitle className="text-lg font-medium">История решений</SheetTitle>
|
||||||
|
<SheetClose asChild>
|
||||||
|
<Button variant="ghost" size="icon" className="h-8 w-8">
|
||||||
|
<X className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</SheetClose>
|
||||||
|
</div>
|
||||||
|
</SheetHeader>
|
||||||
|
|
||||||
|
<div className="flex flex-col mt-3 space-y-2.5 overflow-y-auto max-h-[calc(100vh-80px)] px-4">
|
||||||
|
{solutions.length > 0 ? (
|
||||||
|
solutions.map((solution, index) => (
|
||||||
|
<div key={index} className="w-full">
|
||||||
|
<SolutionStatus solution={solution} />
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<div className="text-center py-8 text-gray-500">
|
||||||
|
У вас пока нет истории решений для этой задачи
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</SheetContent>
|
||||||
|
</Sheet>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SolutionHistorySheet;
|
||||||
+27
-10
@@ -1,24 +1,41 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Task } from "@/shared/types";
|
import { Solution, TaskStatus } from "@/shared/types";
|
||||||
import { getTaskBgColor, getTaskTextColor } from '@/pages/CompetitionSession/utils/utils';
|
import { getTaskBgColor, getTaskTextColor } from '@/pages/CompetitionSession/utils/utils';
|
||||||
|
|
||||||
interface SolutionStatusProps {
|
interface SolutionStatusProps {
|
||||||
task: Task;
|
solution: Solution;
|
||||||
}
|
}
|
||||||
|
|
||||||
const SolutionStatus: React.FC<SolutionStatusProps> = ({ task }) => {
|
const SolutionStatus: React.FC<SolutionStatusProps> = ({ solution }) => {
|
||||||
|
const getStatusText = (status: TaskStatus, score?: number, maxScore?: number) => {
|
||||||
|
switch (status) {
|
||||||
|
case 'checking':
|
||||||
|
return 'На проверке';
|
||||||
|
case 'wrong':
|
||||||
|
return 'Неверный ответ';
|
||||||
|
case 'correct':
|
||||||
|
return `Зачтено ${maxScore}/${maxScore} баллов`;
|
||||||
|
case 'partial':
|
||||||
|
return `Зачтено ${score}/${maxScore} баллов`;
|
||||||
|
case 'uncleared':
|
||||||
|
return 'Не решено';
|
||||||
|
default:
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`${getTaskBgColor(task.status)} rounded-lg p-4 relative`}>
|
<div className={`${getTaskBgColor(solution.status)} rounded-lg p-4 relative`}>
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<span className={`${getTaskTextColor(task.status)} font-medium`}>
|
<span className={`${getTaskTextColor(solution.status)} font-medium`}>
|
||||||
Решение 12345
|
Решение {solution.id}
|
||||||
</span>
|
</span>
|
||||||
<span className={`${getTaskTextColor(task.status)} mt-1`}>
|
<span className={`${getTaskTextColor(solution.status)} mt-1`}>
|
||||||
Зачтено 5/10 баллов
|
{getStatusText(solution.status, solution.score, solution.maxScore)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className={`absolute bottom-2 right-3 text-xs ${getTaskTextColor(task.status)}`}>
|
<div className={`absolute bottom-2 right-3 text-xs ${getTaskTextColor(solution.status)}`}>
|
||||||
1 марта, 08:41
|
{solution.date}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { useState, useRef } from 'react';
|
import React, { useState, useRef } from 'react';
|
||||||
import { Task } from "@/shared/types";
|
import { Solution, Task } from "@/shared/types";
|
||||||
import SolutionStatus from './components/SolutionStatus';
|
import SolutionStatus from './components/SolutionStatus';
|
||||||
import InputSolution from './components/InputSolution';
|
import InputSolution from './components/InputSolution';
|
||||||
import FileSolution from './components/FileSolution';
|
import FileSolution from './components/FileSolution';
|
||||||
@@ -8,6 +8,7 @@ import ActionButtons from './components/ActionButtons';
|
|||||||
|
|
||||||
interface TaskSolutionProps {
|
interface TaskSolutionProps {
|
||||||
task: Task;
|
task: Task;
|
||||||
|
solutions: Solution[];
|
||||||
answer: string;
|
answer: string;
|
||||||
setAnswer: (value: string) => void;
|
setAnswer: (value: string) => void;
|
||||||
onSubmit: () => void;
|
onSubmit: () => void;
|
||||||
@@ -16,6 +17,7 @@ interface TaskSolutionProps {
|
|||||||
|
|
||||||
const TaskSolution: React.FC<TaskSolutionProps> = ({
|
const TaskSolution: React.FC<TaskSolutionProps> = ({
|
||||||
task,
|
task,
|
||||||
|
solutions,
|
||||||
answer,
|
answer,
|
||||||
setAnswer,
|
setAnswer,
|
||||||
onSubmit,
|
onSubmit,
|
||||||
@@ -26,7 +28,7 @@ const TaskSolution: React.FC<TaskSolutionProps> = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="md:w-[500px] flex flex-col gap-4">
|
<div className="md:w-[500px] flex flex-col gap-4">
|
||||||
<SolutionStatus task={task} />
|
<SolutionStatus solution={solutions[0]} />
|
||||||
|
|
||||||
{task.solutionType === 'input' && (
|
{task.solutionType === 'input' && (
|
||||||
<InputSolution answer={answer} setAnswer={setAnswer} />
|
<InputSolution answer={answer} setAnswer={setAnswer} />
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useState, useEffect } from "react";
|
import { useState } from "react";
|
||||||
import { Competition, CompetitionStatus } from "@/shared/types";
|
import { Competition, CompetitionStatus } from "@/shared/types";
|
||||||
import { CompetitionGrid } from "./modules/CompetitionGrid";
|
import { CompetitionGrid } from "./modules/CompetitionGrid";
|
||||||
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Competition, CompetitionStatus, Task } from "../types";
|
import { Competition, CompetitionStatus, Solution, Task } from "../types";
|
||||||
|
|
||||||
const mockCompetitions: Competition[] = [
|
const mockCompetitions: Competition[] = [
|
||||||
{
|
{
|
||||||
@@ -104,4 +104,33 @@ const mockTasks: Task[] = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
|
|
||||||
export { mockCompetitions, mockTasks };
|
const mockSolutions: Solution[] = [
|
||||||
|
{
|
||||||
|
id: '1',
|
||||||
|
status: 'wrong',
|
||||||
|
date: '1 марта, 08:41',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '2',
|
||||||
|
status: 'partial',
|
||||||
|
score: 5,
|
||||||
|
maxScore: 10,
|
||||||
|
date: '28 февраля, 15:22',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '3',
|
||||||
|
status: 'correct',
|
||||||
|
score: 0,
|
||||||
|
maxScore: 10,
|
||||||
|
date: '27 февраля, 12:10',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '4',
|
||||||
|
status: 'checking',
|
||||||
|
date: '1 марта, 08:41',
|
||||||
|
},
|
||||||
|
|
||||||
|
];
|
||||||
|
|
||||||
|
|
||||||
|
export { mockCompetitions, mockTasks, mockSolutions };
|
||||||
|
|||||||
@@ -16,6 +16,13 @@ interface Competition {
|
|||||||
type TaskStatus = "uncleared" | "checking" | "correct" | "partial" | "wrong";
|
type TaskStatus = "uncleared" | "checking" | "correct" | "partial" | "wrong";
|
||||||
type SolutionType = "input" | "file" | "code";
|
type SolutionType = "input" | "file" | "code";
|
||||||
|
|
||||||
|
interface Solution {
|
||||||
|
id: string,
|
||||||
|
status: TaskStatus,
|
||||||
|
date: string,
|
||||||
|
score?: number,
|
||||||
|
maxScore?: number,
|
||||||
|
}
|
||||||
interface Task {
|
interface Task {
|
||||||
id: string;
|
id: string;
|
||||||
number: string;
|
number: string;
|
||||||
@@ -24,4 +31,4 @@ interface Task {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export { CompetitionStatus };
|
export { CompetitionStatus };
|
||||||
export type { Competition, TaskStatus, Task };
|
export type { Solution, Competition, TaskStatus, Task };
|
||||||
|
|||||||
Reference in New Issue
Block a user