mirror of
https://gitlab.com/megazordpobeda/DataRush.git
synced 2026-05-23 01:37:11 +00:00
Merge remote-tracking branch 'origin/master'
This commit is contained in:
+2
-3
@@ -87,6 +87,7 @@ deploy:
|
||||
stage: deploy
|
||||
rules:
|
||||
- if: '$CI_COMMIT_REF_NAME == "master"'
|
||||
when: manual
|
||||
variables:
|
||||
SSH_HOST: "158.160.172.23"
|
||||
SSH_USER: "ubuntu"
|
||||
@@ -107,12 +108,11 @@ deploy:
|
||||
- |
|
||||
ssh $SSH_ADDRESS <<'EOF'
|
||||
cd ~/deploy
|
||||
docker system prune -a --force
|
||||
docker compose pull > deploy.log 2>&1
|
||||
docker compose down >> deploy.log 2>&1
|
||||
docker compose up -d --remove-orphans --force-recreate >> deploy.log 2>&1
|
||||
docker compose ps >> deploy.log 2>&1
|
||||
EOF
|
||||
- ssh $SSH_ADDRESS "docker system prune -a --force"
|
||||
retry: 2
|
||||
|
||||
|
||||
@@ -146,5 +146,4 @@ reset-compose:
|
||||
docker compose up -d --remove-orphans --force-recreate >> deploy.log 2>&1
|
||||
docker compose ps >> deploy.log 2>&1
|
||||
EOF
|
||||
- ssh $SSH_ADDRESS "docker system prune -a --force"
|
||||
retry: 2
|
||||
|
||||
+1
-2
@@ -372,8 +372,7 @@ services:
|
||||
|
||||
custom_python:
|
||||
image: gitlab.prodcontest.ru:5050/team-15/project/custom-python:latest
|
||||
entrypoint: [""]
|
||||
command: echo "Image pulled."
|
||||
entrypoint: ["sh", "-c", "exit 0"]
|
||||
|
||||
checker:
|
||||
image: gitlab.prodcontest.ru:5050/team-15/project/checker:latest
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Generated by Django 5.1.6 on 2025-03-02 22:53
|
||||
# Generated by Django 5.1.6 on 2025-03-03 07:20
|
||||
|
||||
import apps.achievement.models
|
||||
import django.db.models.deletion
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Generated by Django 5.1.6 on 2025-03-02 22:53
|
||||
# Generated by Django 5.1.6 on 2025-03-03 07:20
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Generated by Django 5.1.6 on 2025-03-02 22:53
|
||||
# Generated by Django 5.1.6 on 2025-03-03 07:20
|
||||
|
||||
import apps.competition.models
|
||||
import datetime
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Generated by Django 5.1.6 on 2025-03-02 22:53
|
||||
# Generated by Django 5.1.6 on 2025-03-03 07:20
|
||||
|
||||
import uuid
|
||||
from django.db import migrations, models
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Generated by Django 5.1.6 on 2025-03-02 22:53
|
||||
# Generated by Django 5.1.6 on 2025-03-03 07:20
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
@@ -4,6 +4,7 @@ from apps.task.models import (
|
||||
CompetitionTask,
|
||||
CompetitionTaskAttachment,
|
||||
CompetitionTaskSubmission,
|
||||
CompetitionTaskCriteria
|
||||
)
|
||||
|
||||
|
||||
@@ -12,12 +13,17 @@ class CompletionAttachmentInline(admin.StackedInline):
|
||||
extra = 0
|
||||
|
||||
|
||||
class CompetitionCriteriaInline(admin.StackedInline):
|
||||
model = CompetitionTaskCriteria
|
||||
extra = 0
|
||||
|
||||
|
||||
@admin.register(CompetitionTask)
|
||||
class CompetitionTaskAdmin(admin.ModelAdmin):
|
||||
list_display = ("title", "type", "points")
|
||||
filter_horizontal = ("reviewers",)
|
||||
list_filter = ("type",)
|
||||
inlines = (CompletionAttachmentInline,)
|
||||
inlines = (CompletionAttachmentInline, CompetitionCriteriaInline,)
|
||||
|
||||
|
||||
@admin.register(CompetitionTaskSubmission)
|
||||
@@ -39,9 +45,6 @@ class CompetitionTaskSubmissionAdmin(admin.ModelAdmin):
|
||||
def has_add_permission(self, request, obj=None):
|
||||
return False
|
||||
|
||||
def has_delete_permission(self, request, obj=None):
|
||||
return False
|
||||
|
||||
|
||||
class CompetitionTaskInline(admin.StackedInline):
|
||||
model = CompetitionTask
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Generated by Django 5.1.6 on 2025-03-02 22:53
|
||||
# Generated by Django 5.1.6 on 2025-03-03 07:20
|
||||
|
||||
import apps.task.models
|
||||
import django.db.models.deletion
|
||||
|
||||
@@ -2,7 +2,7 @@ from uuid import uuid4
|
||||
|
||||
from django.db import models
|
||||
from django.db.models import Count, Q
|
||||
from martor.models import MartorField
|
||||
from mdeditor.fields import MDTextField
|
||||
|
||||
from apps.competition.models import Competition
|
||||
from apps.core.models import BaseModel
|
||||
@@ -19,11 +19,15 @@ class CompetitionTask(BaseModel):
|
||||
def answer_file_upload_to(instance, filename) -> str:
|
||||
return f"tasks/{instance.id}/answer/{uuid4()}/{filename}"
|
||||
|
||||
in_competition_position = models.PositiveSmallIntegerField()
|
||||
competition = models.ForeignKey(Competition, on_delete=models.CASCADE)
|
||||
in_competition_position = models.PositiveSmallIntegerField(
|
||||
verbose_name="позиция в соревновании"
|
||||
)
|
||||
competition = models.ForeignKey(Competition, on_delete=models.CASCADE,
|
||||
verbose_name="привязанное соревнование")
|
||||
title = models.CharField(verbose_name="заголовок", max_length=50)
|
||||
description = MartorField(verbose_name="описание")
|
||||
max_attempts = models.PositiveSmallIntegerField(null=True, blank=True)
|
||||
description = MDTextField(verbose_name="описание")
|
||||
max_attempts = models.PositiveSmallIntegerField(null=True, blank=True,
|
||||
verbose_name="максимальное кол-во попыток")
|
||||
type = models.CharField(
|
||||
choices=CompetitionTaskType, max_length=8, verbose_name="тип проверки"
|
||||
)
|
||||
@@ -34,9 +38,10 @@ class CompetitionTask(BaseModel):
|
||||
null=True,
|
||||
blank=True,
|
||||
verbose_name="файл с правильным ответом",
|
||||
help_text="Имеет смысл только при автоматической (ввод ответа или кода) проверке.",
|
||||
)
|
||||
points = models.IntegerField(
|
||||
null=True, blank=True, verbose_name="баллы за задание"
|
||||
null=True, blank=True, verbose_name="общий балл за задание"
|
||||
)
|
||||
|
||||
# only when "checker" type
|
||||
@@ -44,7 +49,10 @@ class CompetitionTask(BaseModel):
|
||||
null=True,
|
||||
blank=True,
|
||||
verbose_name="куда сделать вывод программы участнику",
|
||||
help_text="Путь до файла в котором ожидается результат. Пример: stdout или ./output.txt",
|
||||
help_text=(
|
||||
"Путь до файла в котором ожидается результат. "
|
||||
"Пример: stdout или ./output.txt. Имеет смысл только при автоматическом типе проверки."
|
||||
),
|
||||
default="stdout",
|
||||
)
|
||||
|
||||
@@ -53,10 +61,14 @@ class CompetitionTask(BaseModel):
|
||||
Reviewer,
|
||||
blank=True,
|
||||
verbose_name="ревьюверы",
|
||||
help_text="Справа отображаются действующие проверяющие, слева - доступные для выбора. Для перемещения можно кликнуть 2 раза по проверяющему",
|
||||
help_text=(
|
||||
"Справа отображаются действующие проверяющие, слева - доступные для выбора. "
|
||||
"Для перемещения можно кликнуть 2 раза по проверяющему. Имеет смысл только"
|
||||
" при ручном типе проверки."
|
||||
),
|
||||
)
|
||||
submission_reviewers_count = models.PositiveSmallIntegerField(
|
||||
default=1, null=True, blank=True
|
||||
default=1, null=True, blank=True, verbose_name="кол-во проверяющих для зачета задачи"
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
@@ -72,10 +84,25 @@ class CompetitionTaskCriteria(BaseModel):
|
||||
CompetitionTask, on_delete=models.CASCADE, related_name="criteries"
|
||||
)
|
||||
|
||||
name = models.TextField()
|
||||
slug = models.SlugField()
|
||||
description = models.TextField()
|
||||
max_value = models.PositiveSmallIntegerField()
|
||||
name = models.TextField(
|
||||
verbose_name="название"
|
||||
)
|
||||
slug = models.SlugField(
|
||||
verbose_name="техническое название"
|
||||
)
|
||||
description = models.TextField(
|
||||
verbose_name="описание критерии"
|
||||
)
|
||||
max_value = models.PositiveSmallIntegerField(
|
||||
verbose_name="максимальное кол-во баллов"
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
class Meta:
|
||||
verbose_name = "критерий"
|
||||
verbose_name_plural = "критерии"
|
||||
|
||||
|
||||
class CompetitionTaskAttachment(BaseModel):
|
||||
|
||||
@@ -2,27 +2,38 @@ import httpx
|
||||
from celery import shared_task
|
||||
from django.conf import settings
|
||||
from django.core.files.base import ContentFile
|
||||
import hashlib
|
||||
|
||||
from apps.task.models import CompetitionTaskSubmission
|
||||
|
||||
|
||||
@shared_task(bind=True, max_retries=3)
|
||||
def analyze_data_task(self, submission_id):
|
||||
from .models import CompetitionTaskSubmission
|
||||
|
||||
submission = CompetitionTaskSubmission.objects.get(id=submission_id)
|
||||
try:
|
||||
code = submission.content.read().decode()
|
||||
code_url = (
|
||||
f"{settings.MINIO_DEFAULT_CUSTOM_ENDPOINT_URL}{submission.path}"
|
||||
)
|
||||
files = [
|
||||
(f.name, f.file.open("rb"))
|
||||
for f in submission.task.attachments.filter(public=True)
|
||||
{
|
||||
"url": f"{settings.MINIO_DEFAULT_CUSTOM_ENDPOINT_URL}{attachment.path}",
|
||||
"bind_path": attachment.bind_at,
|
||||
}
|
||||
for attachment in submission.task.attachments.filter(
|
||||
bind_path__isnull=False
|
||||
)
|
||||
]
|
||||
|
||||
response = httpx.post(
|
||||
f"{settings.CHECKER_API_ENDPOINT}/execute",
|
||||
files=[("files", (f.name, f)) for f in files]
|
||||
+ [
|
||||
("code", code),
|
||||
("expected_hash", submission.task.correct_answer_hash),
|
||||
],
|
||||
json={
|
||||
"files": files,
|
||||
"code_url": code_url,
|
||||
"answer_file_path": submission.task.answer_file_path,
|
||||
"expected_hash": hashlib.sha256(
|
||||
submission.task.correct_answer_file.read().encode()
|
||||
).hexdigest(),
|
||||
},
|
||||
timeout=30,
|
||||
)
|
||||
response.raise_for_status()
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Generated by Django 5.1.6 on 2025-03-02 22:53
|
||||
# Generated by Django 5.1.6 on 2025-03-03 07:20
|
||||
|
||||
import django.db.models.deletion
|
||||
import uuid
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Generated by Django 5.1.6 on 2025-03-02 22:53
|
||||
# Generated by Django 5.1.6 on 2025-03-03 07:20
|
||||
|
||||
import uuid
|
||||
from django.db import migrations, models
|
||||
|
||||
@@ -5,9 +5,9 @@ from apps.achievement.models import Achievement
|
||||
from apps.core.models import BaseModel
|
||||
|
||||
|
||||
class UserRole(models.Choices):
|
||||
STUDENT = "student"
|
||||
METODIST = "metodist"
|
||||
class UserRole(models.TextChoices):
|
||||
STUDENT = "student", "Участник соревнований"
|
||||
METODIST = "metodist", "Методист (составитель заданий)"
|
||||
|
||||
|
||||
class User(BaseModel):
|
||||
@@ -15,7 +15,7 @@ class User(BaseModel):
|
||||
username = models.SlugField(unique=True, verbose_name="юзернейм")
|
||||
password = models.TextField(verbose_name="пароль")
|
||||
|
||||
created_at = models.DateTimeField(auto_now=True)
|
||||
created_at = models.DateTimeField(auto_now=True, verbose_name="дата создания")
|
||||
|
||||
achievements = models.ManyToManyField(
|
||||
Achievement, blank=True, verbose_name="ачивки пользователя"
|
||||
@@ -29,7 +29,8 @@ class User(BaseModel):
|
||||
return check_password(self.password, password)
|
||||
|
||||
status = models.CharField(
|
||||
max_length=10, choices=UserRole, default="student"
|
||||
max_length=10, choices=UserRole.choices, default="student",
|
||||
verbose_name="роль участника"
|
||||
)
|
||||
|
||||
def __str__(self) -> str:
|
||||
|
||||
@@ -271,6 +271,8 @@ DEFAULT_CHARSET = "utf-8"
|
||||
|
||||
FORCE_SCRIPT_NAME = None
|
||||
|
||||
X_FRAME_OPTIONS = "SAMEORIGIN"
|
||||
|
||||
INTERNAL_IPS = env(
|
||||
"DJANGO_INTERNAL_IPS",
|
||||
list,
|
||||
@@ -438,8 +440,7 @@ INSTALLED_APPS = [
|
||||
"django_guid",
|
||||
"ninja",
|
||||
"minio_storage",
|
||||
"tinymce",
|
||||
"martor",
|
||||
"mdeditor",
|
||||
# Internal apps
|
||||
"apps.core",
|
||||
"apps.user",
|
||||
@@ -450,65 +451,6 @@ INSTALLED_APPS = [
|
||||
"apps.achievement",
|
||||
]
|
||||
|
||||
# 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,markdown",
|
||||
"toolbar": "undo redo | formatselect | "
|
||||
"bold italic backcolor | alignleft aligncenter "
|
||||
"alignright alignjustify | bullist numlist outdent indent | "
|
||||
"removeformat | help",
|
||||
"skin": "oxide-dark",
|
||||
"content_css": "dark",
|
||||
"textpattern_patterns": [
|
||||
{"start": "*", "end": "*", "format": "italic"},
|
||||
{"start": "**", "end": "**", "format": "bold"},
|
||||
{"start": "#", "format": "h1"},
|
||||
{"start": "##", "format": "h2"},
|
||||
{"start": "###", "format": "h3"},
|
||||
{"start": "####", "format": "h4"},
|
||||
{"start": "#####", "format": "h5"},
|
||||
{"start": "######", "format": "h6"},
|
||||
{"start": "1. ", "cmd": "InsertOrderedList"},
|
||||
{"start": "* ", "cmd": "InsertUnorderedList"},
|
||||
{"start": "- ", "cmd": "InsertUnorderedList"},
|
||||
],
|
||||
}
|
||||
|
||||
# martor
|
||||
|
||||
MARTOR_THEME = "bootstrap"
|
||||
|
||||
MARTOR_ENABLE_CONFIGS = {
|
||||
"emoji": "true", # to enable/disable emoji icons.
|
||||
"imgur": "true", # to enable/disable imgur/custom uploader.
|
||||
"mention": "false", # to enable/disable mention
|
||||
"jquery": "true", # to include/revoke jquery (require for admin default django)
|
||||
"living": "false", # to enable/disable live updates in preview
|
||||
"spellcheck": "false", # to enable/disable spellcheck in form textareas
|
||||
"hljs": "true", # to enable/disable hljs highlighting in preview
|
||||
}
|
||||
|
||||
MARTOR_TOOLBAR_BUTTONS = [
|
||||
"bold",
|
||||
"italic",
|
||||
"horizontal",
|
||||
"heading",
|
||||
"pre-code",
|
||||
"blockquote",
|
||||
"unordered-list",
|
||||
"ordered-list",
|
||||
"link",
|
||||
"emoji",
|
||||
"direct-mention",
|
||||
"toggle-maximize",
|
||||
"help",
|
||||
]
|
||||
|
||||
# GUID
|
||||
|
||||
DJANGO_GUID = {
|
||||
|
||||
@@ -12,10 +12,8 @@ admin.site.index_title = "DataRush"
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
# tinymce
|
||||
path("tinymce/", include("tinymce.urls")),
|
||||
# martor
|
||||
path("martor/", include("martor.urls")),
|
||||
# mdeditor
|
||||
path(r'mdeditor/', include('mdeditor.urls')),
|
||||
# Admin urls
|
||||
path("admin/", admin.site.urls),
|
||||
# API urls
|
||||
|
||||
@@ -12,14 +12,13 @@ dependencies = [
|
||||
"django-extensions>=3.2.3",
|
||||
"django-guid>=3.5.0",
|
||||
"django-health-check>=3.18.3",
|
||||
"django-mdeditor>=0.1.20",
|
||||
"django-minio-storage>=0.5.7",
|
||||
"django-ninja>=1.3.0",
|
||||
"django-pagedown>=2.2.1",
|
||||
"django-stubs-ext>=5.1.3",
|
||||
"django-tinymce>=4.1.0",
|
||||
"gunicorn>=23.0.0",
|
||||
"httpx>=0.28.1",
|
||||
"martor>=1.6.45",
|
||||
"pillow>=11.1.0",
|
||||
"psycopg2-binary>=2.9.10",
|
||||
"pydantic>=2.10.5",
|
||||
|
||||
@@ -9,3 +9,5 @@ fi
|
||||
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
|
||||
@@ -30,7 +30,7 @@ ENV PYTHONDONTWRITEBYTECODE=1 \
|
||||
PYTHONOPTIMIZE=2 \
|
||||
PATH="/opt/venv/bin:$PATH"
|
||||
|
||||
EXPOSE 8080
|
||||
EXPOSE 8000
|
||||
|
||||
HEALTHCHECK --interval=30s --timeout=5s --start-period=5s --start-interval=2s --retries=3 \
|
||||
CMD wget --no-verbose --tries=1 --spider http://127.0.0.1:8000/health || exit 1
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import docker.errors
|
||||
from fastapi import FastAPI, HTTPException, status
|
||||
from pydantic import BaseModel, Field, HttpUrl
|
||||
import aiohttp
|
||||
@@ -158,12 +157,12 @@ def run_container_safely(
|
||||
"stderr": f"Container error: {str(e)}",
|
||||
"status": -1,
|
||||
}
|
||||
finally:
|
||||
if container:
|
||||
try:
|
||||
container.remove(force=True)
|
||||
except docker.errors.DockerException:
|
||||
pass
|
||||
# finally:
|
||||
# if container:
|
||||
# try:
|
||||
# container.remove(force=True)
|
||||
# except docker.errors.DockerException:
|
||||
# pass
|
||||
|
||||
|
||||
def validate_file_path(path: str) -> bool:
|
||||
|
||||
+1
@@ -22,6 +22,7 @@ const SolutionHistorySheet: React.FC<SolutionHistorySheetProps> = ({
|
||||
onSolutionSelect,
|
||||
currentSolutionId
|
||||
}) => {
|
||||
|
||||
return (
|
||||
<Sheet open={isOpen} onOpenChange={onOpenChange}>
|
||||
<SheetContent className="w-[350px] sm:w-[450px] p-0">
|
||||
|
||||
@@ -32,7 +32,7 @@ const TaskSolution: React.FC<TaskSolutionProps> = ({
|
||||
const [selectedSolutionUrl, setSelectedSolutionUrl] = useState<string | null>(null);
|
||||
const [currentSolution, setCurrentSolution] = useState<Solution | null>(null);
|
||||
const { id: competitionId } = useParams<{ id: string }>();
|
||||
const previousTaskIdRef = useRef<string | null>(null);
|
||||
const taskIdRef = useRef<string | null>(null);
|
||||
|
||||
const solutionsQuery = useQuery({
|
||||
queryKey: ['solutionHistory', competitionId, task.id],
|
||||
@@ -41,36 +41,38 @@ const TaskSolution: React.FC<TaskSolutionProps> = ({
|
||||
});
|
||||
|
||||
const solutionHistory = solutionsQuery.data || [];
|
||||
|
||||
// Handle task changes
|
||||
useEffect(() => {
|
||||
if (solutionHistory.length > 0 && !currentSolution) {
|
||||
setCurrentSolution(solutionHistory[solutionHistory.length - 1]);
|
||||
}
|
||||
}, [solutionHistory, currentSolution]);
|
||||
|
||||
useEffect(() => {
|
||||
if (solutionHistory.length > 0 && currentSolution &&
|
||||
solutionHistory[0].id !== currentSolution.id) {
|
||||
setCurrentSolution(solutionHistory[solutionHistory.length - 1]);
|
||||
}
|
||||
}, [solutionHistory, currentSolution]);
|
||||
|
||||
useEffect(() => {
|
||||
if (previousTaskIdRef.current !== task.id) {
|
||||
if (taskIdRef.current !== task.id) {
|
||||
setCurrentSolution(null);
|
||||
setSelectedSolutionUrl(null);
|
||||
|
||||
setAnswer("");
|
||||
setSelectedFile(null);
|
||||
taskIdRef.current = task.id;
|
||||
|
||||
if (solutionHistory.length > 0 && !solutionsQuery.isLoading) {
|
||||
setCurrentSolution(solutionHistory[solutionHistory.length - 1]);
|
||||
// Wait for the query to complete
|
||||
if (!solutionsQuery.isLoading && solutionHistory.length > 0) {
|
||||
// Get the most recent solution (last in the array)
|
||||
const latestSolution = solutionHistory[solutionHistory.length - 1];
|
||||
setCurrentSolution(latestSolution);
|
||||
}
|
||||
|
||||
previousTaskIdRef.current = task.id;
|
||||
}
|
||||
}, [task.id, solutionHistory, solutionsQuery.isLoading, setAnswer, setSelectedFile]);
|
||||
|
||||
// Refresh current solution when the solution history changes (after a new submission)
|
||||
useEffect(() => {
|
||||
if (!solutionsQuery.isLoading && solutionHistory.length > 0) {
|
||||
// If we don't have a current solution or there's a new submission
|
||||
// (which would be the last item in the array)
|
||||
if (!currentSolution ||
|
||||
currentSolution.id !== solutionHistory[solutionHistory.length - 1].id) {
|
||||
// Set to the latest solution (last in the array)
|
||||
setCurrentSolution(solutionHistory[solutionHistory.length - 1]);
|
||||
}
|
||||
}
|
||||
}, [solutionHistory, currentSolution, solutionsQuery.isLoading]);
|
||||
|
||||
// Load solution content when current solution changes
|
||||
useEffect(() => {
|
||||
const loadSolutionContent = async () => {
|
||||
if (!currentSolution || !currentSolution.content) return;
|
||||
@@ -108,6 +110,10 @@ const TaskSolution: React.FC<TaskSolutionProps> = ({
|
||||
setSelectedSolutionUrl(null);
|
||||
};
|
||||
|
||||
const handleSubmitWrapper = () => {
|
||||
onSubmit();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="md:w-[500px] flex flex-col gap-4">
|
||||
{currentSolution ? (
|
||||
@@ -143,7 +149,7 @@ const TaskSolution: React.FC<TaskSolutionProps> = ({
|
||||
)}
|
||||
|
||||
<ActionButtons
|
||||
onSubmit={onSubmit}
|
||||
onSubmit={handleSubmitWrapper}
|
||||
onHistoryClick={handleOpenHistory}
|
||||
/>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user