From 5e8742ee108ba6f9c11293c2d1597ded6babb7e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BD=D0=B4=D1=80=D0=B5=D0=B9=20=D0=A1=D1=83=D0=BC?= =?UTF-8?q?=D0=B8=D0=BD?= Date: Sun, 2 Mar 2025 13:34:54 +0300 Subject: [PATCH 1/9] fix: fixed admin bug and added 12rojgpouehgsrfojn --- .../competition/migrations/0001_initial.py | 2 +- .../core/management/commands/generate_data.py | 17 ++++---- .../apps/review/migrations/0001_initial.py | 30 ++++++-------- .../apps/review/migrations/0002_initial.py | 27 ++++++++++++ services/backend/apps/review/models.py | 2 +- services/backend/apps/task/admin.py | 2 +- .../apps/task/migrations/0001_initial.py | 14 ++++--- ...02_competitiontask_attachments_and_more.py | 40 ------------------ ...0003_remove_competitiontask_attachments.py | 17 -------- services/backend/apps/task/models.py | 41 +++++++------------ .../apps/team/migrations/0001_initial.py | 2 +- .../apps/user/migrations/0001_initial.py | 3 +- .../user/migrations/0002_user_created_at.py | 18 -------- 13 files changed, 75 insertions(+), 140 deletions(-) create mode 100644 services/backend/apps/review/migrations/0002_initial.py delete mode 100644 services/backend/apps/task/migrations/0002_competitiontask_attachments_and_more.py delete mode 100644 services/backend/apps/task/migrations/0003_remove_competitiontask_attachments.py delete mode 100644 services/backend/apps/user/migrations/0002_user_created_at.py diff --git a/services/backend/apps/competition/migrations/0001_initial.py b/services/backend/apps/competition/migrations/0001_initial.py index 223f6b8..1ada8da 100644 --- a/services/backend/apps/competition/migrations/0001_initial.py +++ b/services/backend/apps/competition/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 5.1.6 on 2025-03-02 06:13 +# Generated by Django 5.1.6 on 2025-03-02 10:28 import apps.competition.models import datetime diff --git a/services/backend/apps/core/management/commands/generate_data.py b/services/backend/apps/core/management/commands/generate_data.py index bc3b9de..0b8b31c 100644 --- a/services/backend/apps/core/management/commands/generate_data.py +++ b/services/backend/apps/core/management/commands/generate_data.py @@ -20,8 +20,8 @@ class Command(BaseCommand): self.stdout.write("Starting data generation...") users = self.create_users(5) competitions = self.create_competitions(2, users) + self.reviewers = self.create_reviewers(2) tasks = self.create_tasks(competitions) - self.reviewers = self.create_reviewers(1) self.create_submissions(tasks, users) self.create_states(competitions, users) self.stdout.write("Data generation completed.") @@ -108,8 +108,14 @@ class Command(BaseCommand): ) tasks.append(task) self.stdout.write(f"Created task: {title} (type: {task_type})") + self.add_reviewers_to_task(tasks) return tasks + def add_reviewers_to_task(self, tasks): + for task in tasks: + task.reviewers.set(self.reviewers) + task.save() + def create_submissions(self, tasks, users): for task in tasks: # Each task will get between 1 and 3 submissions @@ -133,15 +139,6 @@ class Command(BaseCommand): self.stdout.write( f"Created submission for task '{task.title}' by user '{user.username}'" ) - self.add_reviewers(submission) - - def add_reviewers(self, submission): - for reviewer in self.reviewers: - if random.choice([True, False]): - Review.objects.create( - submission=submission, - reviewer=reviewer, - ) def create_states(self, competitions, users): # For each competition, create a State for some of its participants diff --git a/services/backend/apps/review/migrations/0001_initial.py b/services/backend/apps/review/migrations/0001_initial.py index 70108e7..1d0ac7b 100644 --- a/services/backend/apps/review/migrations/0001_initial.py +++ b/services/backend/apps/review/migrations/0001_initial.py @@ -1,6 +1,5 @@ -# Generated by Django 5.1.6 on 2025-03-02 09:31 +# Generated by Django 5.1.6 on 2025-03-02 10:28 -import django.db.models.deletion import uuid from django.db import migrations, models @@ -10,10 +9,21 @@ class Migration(migrations.Migration): initial = True dependencies = [ - ('task', '0003_remove_competitiontask_attachments'), ] operations = [ + migrations.CreateModel( + name='Review', + fields=[ + ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), + ('evaluation', models.JSONField(blank=True, default=list, null=True, verbose_name='выполнение')), + ('state', models.CharField(choices=[('not_checked', 'Not Checked'), ('checking', 'Checking'), ('checked', 'Checked')], default='not_checked', max_length=11, verbose_name='состояние')), + ], + options={ + 'verbose_name': 'проверка', + 'verbose_name_plural': 'проверки', + }, + ), migrations.CreateModel( name='Reviewer', fields=[ @@ -27,18 +37,4 @@ class Migration(migrations.Migration): 'verbose_name_plural': 'проверяющие', }, ), - migrations.CreateModel( - name='Review', - fields=[ - ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), - ('evaluation', models.JSONField(blank=True, default=list, null=True, verbose_name='выполнение')), - ('state', models.CharField(choices=[('not_checked', 'Not Checked'), ('checking', 'Checking'), ('checked', 'Checked')], default='not_checked', max_length=11, verbose_name='состояние')), - ('submission', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='reviews', to='task.competitiontasksubmission', verbose_name='посылка')), - ('reviewer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='review.reviewer', verbose_name='проверяющий')), - ], - options={ - 'verbose_name': 'проверка', - 'verbose_name_plural': 'проверки', - }, - ), ] diff --git a/services/backend/apps/review/migrations/0002_initial.py b/services/backend/apps/review/migrations/0002_initial.py new file mode 100644 index 0000000..2d6ee5c --- /dev/null +++ b/services/backend/apps/review/migrations/0002_initial.py @@ -0,0 +1,27 @@ +# Generated by Django 5.1.6 on 2025-03-02 10:28 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('review', '0001_initial'), + ('task', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='review', + name='submission', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='reviews', to='task.competitiontasksubmission', verbose_name='посылка'), + ), + migrations.AddField( + model_name='review', + name='reviewer', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='review.reviewer', verbose_name='проверяющий'), + ), + ] diff --git a/services/backend/apps/review/models.py b/services/backend/apps/review/models.py index a7ff21f..5fd4fb9 100644 --- a/services/backend/apps/review/models.py +++ b/services/backend/apps/review/models.py @@ -27,7 +27,7 @@ class Review(BaseModel): reviewer = models.ForeignKey(Reviewer, on_delete=models.CASCADE, verbose_name="проверяющий") submission = models.ForeignKey( - "CompetitionTaskSubmission", + "task.CompetitionTaskSubmission", on_delete=models.CASCADE, related_name="reviews", verbose_name="посылка" diff --git a/services/backend/apps/task/admin.py b/services/backend/apps/task/admin.py index ce7d553..dd3ed25 100644 --- a/services/backend/apps/task/admin.py +++ b/services/backend/apps/task/admin.py @@ -19,7 +19,7 @@ class CompetitionTaskSubmissionAdmin(admin.ModelAdmin): list_display = ("task", "user", "status",) search_fields = ("task__id", "task__title", "user__username", "user__email") filter = ("plagiarism_checked",) - ordering = "-timestamp" + ordering = ["-timestamp"] def has_add_permission(self, request, obj=None): return False diff --git a/services/backend/apps/task/migrations/0001_initial.py b/services/backend/apps/task/migrations/0001_initial.py index cf4fbdc..c2cbaa8 100644 --- a/services/backend/apps/task/migrations/0001_initial.py +++ b/services/backend/apps/task/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 5.1.6 on 2025-03-02 06:13 +# Generated by Django 5.1.6 on 2025-03-02 10:28 import apps.task.models import django.db.models.deletion @@ -13,6 +13,7 @@ class Migration(migrations.Migration): dependencies = [ ('competition', '0001_initial'), + ('review', '0001_initial'), ('user', '0001_initial'), ] @@ -30,6 +31,7 @@ class Migration(migrations.Migration): ('points', models.IntegerField(blank=True, null=True, verbose_name='баллы за задание')), ('answer_file_path', models.TextField(blank=True, default='stdout', help_text='Путь до файла в котором ожидается результат. Пример: stdout или ./output.txt', null=True, verbose_name='куда сделать вывод программы участнику')), ('competition', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='competition.competition')), + ('reviewers', models.ManyToManyField(blank=True, to='review.reviewer')), ], options={ 'verbose_name': 'задание', @@ -40,10 +42,10 @@ class Migration(migrations.Migration): name='CompetitionTaskAttachment', fields=[ ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), - ('file', models.FileField(upload_to=apps.task.models.CompetitionTaskAttachment.file_upload_at)), - ('bind_at', models.FilePathField()), - ('public', models.BooleanField(default=False)), - ('task', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='task.competitiontask')), + ('file', models.FileField(upload_to=apps.task.models.CompetitionTaskAttachment.file_upload_at, verbose_name='файл')), + ('bind_at', models.FilePathField(verbose_name='путь сохранения')), + ('public', models.BooleanField(default=False, verbose_name='публичный')), + ('task', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='task.competitiontask', verbose_name='задание')), ], options={ 'abstract': False, @@ -67,7 +69,7 @@ class Migration(migrations.Migration): 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)), + ('status', models.CharField(choices=[('sent', 'Sent'), ('checking', 'Checking'), ('checked', 'Checked')], default='sent', max_length=8, verbose_name='статус')), ('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)), diff --git a/services/backend/apps/task/migrations/0002_competitiontask_attachments_and_more.py b/services/backend/apps/task/migrations/0002_competitiontask_attachments_and_more.py deleted file mode 100644 index 9f88f60..0000000 --- a/services/backend/apps/task/migrations/0002_competitiontask_attachments_and_more.py +++ /dev/null @@ -1,40 +0,0 @@ -# Generated by Django 5.1.6 on 2025-03-02 08:50 - -import apps.task.models -import django.db.models.deletion -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('task', '0001_initial'), - ] - - operations = [ - migrations.AddField( - model_name='competitiontask', - name='attachments', - field=models.ManyToManyField(blank=True, related_name='tasks_attachments', to='task.competitiontaskattachment'), - ), - migrations.AlterField( - model_name='competitiontaskattachment', - name='bind_at', - field=models.FilePathField(verbose_name='путь сохранения'), - ), - migrations.AlterField( - model_name='competitiontaskattachment', - name='file', - field=models.FileField(upload_to=apps.task.models.CompetitionTaskAttachment.file_upload_at, verbose_name='файл'), - ), - migrations.AlterField( - model_name='competitiontaskattachment', - name='public', - field=models.BooleanField(default=False, verbose_name='публичный'), - ), - migrations.AlterField( - model_name='competitiontaskattachment', - name='task', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='task.competitiontask', verbose_name='задание'), - ), - ] diff --git a/services/backend/apps/task/migrations/0003_remove_competitiontask_attachments.py b/services/backend/apps/task/migrations/0003_remove_competitiontask_attachments.py deleted file mode 100644 index 0e5d430..0000000 --- a/services/backend/apps/task/migrations/0003_remove_competitiontask_attachments.py +++ /dev/null @@ -1,17 +0,0 @@ -# Generated by Django 5.1.6 on 2025-03-02 09:31 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('task', '0002_competitiontask_attachments_and_more'), - ] - - operations = [ - migrations.RemoveField( - model_name='competitiontask', - name='attachments', - ), - ] diff --git a/services/backend/apps/task/models.py b/services/backend/apps/task/models.py index ef8e94c..b2741ed 100644 --- a/services/backend/apps/task/models.py +++ b/services/backend/apps/task/models.py @@ -6,7 +6,7 @@ from tinymce.models import HTMLField from apps.competition.models import Competition from apps.core.models import BaseModel -from apps.review.models import Review, ReviewStatusChoices +from apps.review.models import Review, ReviewStatusChoices, Reviewer from apps.user.models import User @@ -50,6 +50,9 @@ class CompetitionTask(BaseModel): default="stdout", ) + # only when "review" type + reviewers = models.ManyToManyField(Reviewer, blank=True) + def __str__(self): return self.title @@ -93,10 +96,8 @@ class CompetitionTaskSubmission(BaseModel): def submission_stdout_upload_to(instance, filename) -> str: return f"/submissions/{instance.id}/stdout" - user = models.ForeignKey(User, on_delete=models.CASCADE, - verbose_name="пользователь") - task = models.ForeignKey(CompetitionTask, on_delete=models.CASCADE, - verbose_name="задание") + user = models.ForeignKey(User, on_delete=models.CASCADE) + task = models.ForeignKey(CompetitionTask, on_delete=models.CASCADE) status = models.CharField( choices=StatusChoices.choices, @@ -106,38 +107,24 @@ class CompetitionTaskSubmission(BaseModel): ) # code or text or file - content = models.FileField(upload_to=submission_content_upload_to, - verbose_name="код/файл посылки") + content = models.FileField(upload_to=submission_content_upload_to) # only if task type is checker stdout = models.FileField( - upload_to=submission_stdout_upload_to, null=True, blank=True, - verbose_name="вывод чекера" + upload_to=submission_stdout_upload_to, null=True, blank=True ) # depends on task type: # - input: {"correct": boolean} # - file: {"total_points": integer, "by_criteria": ["criteria_name": integer]} # - code: {"correct": boolean} - result = models.JSONField(default=None, null=True, blank=True, - verbose_name="результат проверки") + result = models.JSONField(default=None, null=True, blank=True) # just more readable result representation, maybe will be calcuated somehow more complex depends on criteria - earned_points = models.IntegerField(null=True, blank=True, - verbose_name="получено баллов") + earned_points = models.IntegerField(null=True, blank=True) - checked_at = models.DateTimeField(null=True, blank=True, - verbose_name="дата и время проверки") - plagiarism_checked = models.BooleanField(default=False, - verbose_name="проверено на плагиат") - timestamp = models.DateTimeField(auto_now_add=True, - verbose_name="дата отправки") - - def __str__(self): - return str(self.id) - - class Meta: - verbose_name = "посылка" - verbose_name_plural = "посылки" + checked_at = models.DateTimeField(null=True, blank=True) + plagiarism_checked = models.BooleanField(default=False) + timestamp = models.DateTimeField(auto_now_add=True) def send_on_review(self): if not self.task.reviewers.exists(): @@ -158,7 +145,7 @@ class CompetitionTaskSubmission(BaseModel): .order_by("pending_count") .first() ) - Review.objects.create( + review = Review.objects.create( reviewer=reviewer, submission=self, ) diff --git a/services/backend/apps/team/migrations/0001_initial.py b/services/backend/apps/team/migrations/0001_initial.py index 27317a3..7e4fca4 100644 --- a/services/backend/apps/team/migrations/0001_initial.py +++ b/services/backend/apps/team/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 5.1.6 on 2025-03-02 06:13 +# Generated by Django 5.1.6 on 2025-03-02 10:28 import django.db.models.deletion import uuid diff --git a/services/backend/apps/user/migrations/0001_initial.py b/services/backend/apps/user/migrations/0001_initial.py index 12a0407..fe09ceb 100644 --- a/services/backend/apps/user/migrations/0001_initial.py +++ b/services/backend/apps/user/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 5.1.6 on 2025-03-02 00:16 +# Generated by Django 5.1.6 on 2025-03-02 10:28 import uuid from django.db import migrations, models @@ -19,6 +19,7 @@ class Migration(migrations.Migration): ('email', models.EmailField(max_length=254, unique=True, verbose_name='почта')), ('username', models.SlugField(unique=True, verbose_name='юзернейм')), ('password', models.TextField(verbose_name='пароль')), + ('created_at', models.DateTimeField(auto_now=True)), ('status', models.CharField(choices=[('student', 'Student'), ('metodist', 'Metodist')], default='student', max_length=10)), ], options={ diff --git a/services/backend/apps/user/migrations/0002_user_created_at.py b/services/backend/apps/user/migrations/0002_user_created_at.py deleted file mode 100644 index 83094ec..0000000 --- a/services/backend/apps/user/migrations/0002_user_created_at.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 5.1.6 on 2025-03-02 09:50 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('user', '0001_initial'), - ] - - operations = [ - migrations.AddField( - model_name='user', - name='created_at', - field=models.DateTimeField(auto_now=True), - ), - ] From 3814c8a4d679d0466a450bdd10cacfe083f32b87 Mon Sep 17 00:00:00 2001 From: Timur Date: Sun, 2 Mar 2025 13:46:24 +0300 Subject: [PATCH 2/9] fix namings at review admin --- services/backend/apps/review/admin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/backend/apps/review/admin.py b/services/backend/apps/review/admin.py index c35598b..48730de 100644 --- a/services/backend/apps/review/admin.py +++ b/services/backend/apps/review/admin.py @@ -4,7 +4,7 @@ from apps.review.models import Review, Reviewer @admin.register(Reviewer) -class ReviewAdmin(admin.ModelAdmin): +class ReviewersAdmin(admin.ModelAdmin): list_display = ("name", "surname",) search_fields = ("name", "surname",) From b220004ea5293c85bef1b6d2a56d819679b94e53 Mon Sep 17 00:00:00 2001 From: moolcoov Date: Sun, 2 Mar 2025 13:52:42 +0300 Subject: [PATCH 3/9] feat: something with something --- services/frontend/bun.lock | 5 + services/frontend/package.json | 1 + services/frontend/src/App.tsx | 34 +++--- .../frontend/src/components/ui/button.tsx | 5 +- .../components/ui/icons/datarush-review.tsx | 31 ++++++ .../src/components/ui/icons/datarush.tsx | 10 +- .../frontend/src/components/ui/loading.tsx | 9 ++ .../frontend/src/pages/Competition/index.tsx | 41 ++++--- .../src/pages/CompetitionSession/index.tsx | 27 ++--- .../components/CompetitionCard/index.tsx | 32 ++++-- .../components/CompetitionTag/index.tsx | 26 ----- .../frontend/src/pages/Competitions/index.tsx | 103 +++++++++++++----- .../index.tsx => CompetitionsGrid.tsx} | 4 +- .../Competitions/modules/NoCompetitions.tsx | 15 +++ services/frontend/src/pages/Login/index.tsx | 2 +- services/frontend/src/pages/Review/index.tsx | 51 +++++++++ .../pages/Review/modules/review-header.tsx | 27 +++++ services/frontend/src/shared/api/auth.ts | 6 +- .../frontend/src/shared/api/competitions.ts | 14 +++ services/frontend/src/shared/api/index.ts | 5 +- services/frontend/src/shared/api/review.ts | 10 ++ services/frontend/src/shared/api/user.ts | 4 +- .../frontend/src/shared/types/competition.ts | 26 +++++ services/frontend/src/shared/types/review.ts | 5 + services/frontend/src/styles/globals.css | 2 +- 25 files changed, 369 insertions(+), 126 deletions(-) create mode 100644 services/frontend/src/components/ui/icons/datarush-review.tsx create mode 100644 services/frontend/src/components/ui/loading.tsx delete mode 100644 services/frontend/src/pages/Competitions/components/CompetitionTag/index.tsx rename services/frontend/src/pages/Competitions/modules/{CompetitionGrid/index.tsx => CompetitionsGrid.tsx} (80%) create mode 100644 services/frontend/src/pages/Competitions/modules/NoCompetitions.tsx create mode 100644 services/frontend/src/pages/Review/modules/review-header.tsx create mode 100644 services/frontend/src/shared/api/competitions.ts create mode 100644 services/frontend/src/shared/api/review.ts create mode 100644 services/frontend/src/shared/types/competition.ts create mode 100644 services/frontend/src/shared/types/review.ts diff --git a/services/frontend/bun.lock b/services/frontend/bun.lock index 96bf77a..4b0ab6a 100644 --- a/services/frontend/bun.lock +++ b/services/frontend/bun.lock @@ -9,6 +9,7 @@ "@radix-ui/react-slot": "^1.1.2", "@radix-ui/react-tabs": "^1.1.3", "@tailwindcss/vite": "^4.0.9", + "@tanstack/react-query": "^5.66.11", "autoprefixer": "^10.4.20", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", @@ -269,6 +270,10 @@ "@tailwindcss/vite": ["@tailwindcss/vite@4.0.9", "", { "dependencies": { "@tailwindcss/node": "4.0.9", "@tailwindcss/oxide": "4.0.9", "lightningcss": "^1.29.1", "tailwindcss": "4.0.9" }, "peerDependencies": { "vite": "^5.2.0 || ^6" } }, "sha512-BIKJO+hwdIsN7V6I7SziMZIVHWWMsV/uCQKYEbeiGRDRld+TkqyRRl9+dQ0MCXbhcVr+D9T/qX2E84kT7V281g=="], + "@tanstack/query-core": ["@tanstack/query-core@5.66.11", "", {}, "sha512-ZEYxgHUcohj3sHkbRaw0gYwFxjY5O6M3IXOYXEun7E1rqNhsP8fOtqjJTKPZpVHcdIdrmX4lzZctT4+pts0OgA=="], + + "@tanstack/react-query": ["@tanstack/react-query@5.66.11", "", { "dependencies": { "@tanstack/query-core": "5.66.11" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-uPDiQbZScWkAeihmZ9gAm3wOBA1TmLB1KCB1fJ1hIiEKq3dTT+ja/aYM7wGUD+XiEsY4sDSE7p8VIz/21L2Dow=="], + "@types/cookie": ["@types/cookie@0.6.0", "", {}, "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA=="], "@types/debug": ["@types/debug@4.1.12", "", { "dependencies": { "@types/ms": "*" } }, "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ=="], diff --git a/services/frontend/package.json b/services/frontend/package.json index 85a85ec..dee217c 100644 --- a/services/frontend/package.json +++ b/services/frontend/package.json @@ -15,6 +15,7 @@ "@radix-ui/react-slot": "^1.1.2", "@radix-ui/react-tabs": "^1.1.3", "@tailwindcss/vite": "^4.0.9", + "@tanstack/react-query": "^5.66.11", "autoprefixer": "^10.4.20", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", diff --git a/services/frontend/src/App.tsx b/services/frontend/src/App.tsx index de38f23..01df713 100644 --- a/services/frontend/src/App.tsx +++ b/services/frontend/src/App.tsx @@ -8,24 +8,32 @@ import Competition from "./pages/Competition"; import CompetitionSession from "./pages/CompetitionSession"; import LoginPage from "./pages/Login"; import { AuthLayout } from "./widgets/auth-layout"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import ReviewPage from "./pages/Review"; + +const queryClient = new QueryClient(); const App = () => { return ( - - } /> + + + } /> - }> - }> - } /> - } /> + }> + }> + } /> + } /> + + + } + /> + + } /> - - } - /> - - + + ); }; diff --git a/services/frontend/src/components/ui/button.tsx b/services/frontend/src/components/ui/button.tsx index 6a5e720..11b8b06 100644 --- a/services/frontend/src/components/ui/button.tsx +++ b/services/frontend/src/components/ui/button.tsx @@ -14,15 +14,14 @@ const buttonVariants = cva( "bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40", outline: "border border-input bg-background shadow-xs hover:bg-accent hover:text-accent-foreground", - secondary: - "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80", + secondary: "bg-card text-secondary-foreground hover:bg-card/80", ghost: "hover:bg-accent hover:text-accent-foreground", link: "text-primary underline-offset-4 hover:underline", }, size: { default: "h-11 px-4 text-base font-semibold rounded-xl", lg: "h-12 px-5 py-3 has-[>svg]:px-3 text-lg font-semibold", - sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5", + sm: "h-10 rounded-xl gap-1.5 px-5 has-[>svg]:px-2.5", icon: "size-9", }, }, diff --git a/services/frontend/src/components/ui/icons/datarush-review.tsx b/services/frontend/src/components/ui/icons/datarush-review.tsx new file mode 100644 index 0000000..97edab8 --- /dev/null +++ b/services/frontend/src/components/ui/icons/datarush-review.tsx @@ -0,0 +1,31 @@ +export const DataRushReview = ({ + size = 50, + className, +}: { + size?: number; + className?: string; +}) => { + return ( + + + + + + + ); +}; diff --git a/services/frontend/src/components/ui/icons/datarush.tsx b/services/frontend/src/components/ui/icons/datarush.tsx index 0cebbe9..4a0f8f4 100644 --- a/services/frontend/src/components/ui/icons/datarush.tsx +++ b/services/frontend/src/components/ui/icons/datarush.tsx @@ -1,5 +1,5 @@ const DataRush = ({ - size = 52, + size = 50, className, }: { size?: number; @@ -8,18 +8,18 @@ const DataRush = ({ return ( - + diff --git a/services/frontend/src/components/ui/loading.tsx b/services/frontend/src/components/ui/loading.tsx new file mode 100644 index 0000000..7cb1272 --- /dev/null +++ b/services/frontend/src/components/ui/loading.tsx @@ -0,0 +1,9 @@ +import { Spinner } from "./spinner"; + +export const Loading = () => { + return ( +
+ +
+ ); +}; diff --git a/services/frontend/src/pages/Competition/index.tsx b/services/frontend/src/pages/Competition/index.tsx index 080ee46..fa64ec5 100644 --- a/services/frontend/src/pages/Competition/index.tsx +++ b/services/frontend/src/pages/Competition/index.tsx @@ -1,17 +1,28 @@ -import { useState } from "react"; import { useParams, Link, useNavigate } from "react-router-dom"; import { Button } from "@/components/ui/button"; import { ArrowLeft } from "lucide-react"; import ReactMarkdown from "react-markdown"; -import { Competition } from "@/shared/types"; -import { mockCompetitions, mockTasks } from "@/shared/mocks/mocks"; +import { mockTasks } from "@/shared/mocks/mocks"; +import { useQuery } from "@tanstack/react-query"; +import { getCompetition } from "@/shared/api/competitions"; +import { Loading } from "@/components/ui/Loading"; const CompetitionPage = () => { const { id } = useParams<{ id: string }>(); const navigate = useNavigate(); - const [competition] = useState( - mockCompetitions.find((comp) => comp.id === id)!, - ); + + const { data: competition, isLoading } = useQuery({ + queryKey: ["competition", id], + queryFn: async () => getCompetition(id || ""), + }); + + if (isLoading) { + return ; + } + + if (!id || !competition) { + return <>; + } const handleContinue = () => { if (competition?.id) { @@ -35,18 +46,20 @@ const CompetitionPage = () => {
-
- {competition.name} -
+ {competition.image_url && ( +
+ {competition.title} +
+ )}

- {competition.name} + {competition.title}

{competition.description || ""} diff --git a/services/frontend/src/pages/CompetitionSession/index.tsx b/services/frontend/src/pages/CompetitionSession/index.tsx index 2d2b09b..b178b59 100644 --- a/services/frontend/src/pages/CompetitionSession/index.tsx +++ b/services/frontend/src/pages/CompetitionSession/index.tsx @@ -11,7 +11,7 @@ const CompetitionSession = () => { const [tasks] = useState(mockTasks); const [answer, setAnswer] = useState(""); - const currentTask = tasks.find(t => t.id === taskId) || null; + const currentTask = tasks.find((t) => t.id === taskId) || tasks.at(0); if (!taskId && tasks.length > 0) { return ; @@ -20,22 +20,21 @@ const CompetitionSession = () => { const handleSubmit = () => { console.log("Submitting answer:", answer); }; - return ( -
- + - +
-
+
{currentTask ? ( -
+
- { />
) : ( -
-

- Загрузка задания... -

+
+

Загрузка задания...

)}
@@ -56,4 +53,4 @@ const CompetitionSession = () => { ); }; -export default CompetitionSession; \ No newline at end of file +export default CompetitionSession; diff --git a/services/frontend/src/pages/Competitions/components/CompetitionCard/index.tsx b/services/frontend/src/pages/Competitions/components/CompetitionCard/index.tsx index ad5edb0..8ae52eb 100644 --- a/services/frontend/src/pages/Competitions/components/CompetitionCard/index.tsx +++ b/services/frontend/src/pages/Competitions/components/CompetitionCard/index.tsx @@ -1,6 +1,10 @@ -import { Competition, CompetitionStatus } from "@/shared/types"; import { cn } from "@/shared/lib/utils"; import { Card, CardContent } from "@/components/ui/card"; +import { + Competition, + CompetitionState, + CompetitionType, +} from "@/shared/types/competition"; interface CompetitionCardProps { competition: Competition; @@ -16,28 +20,36 @@ export function CompetitionCard({ className={cn("aspect-square h-full w-auto overflow-hidden", className)} >
- {competition.name} + {competition.image_url && ( + {competition.title} + )}
- {competition.isOlympics ? "Олимпиада" : "Тренировка"} - {competition.status != CompetitionStatus.NotParticipating && ( + + {competition.type === CompetitionType.COMPETITIVE + ? "Соревнование" + : "Тренировка"} + + {competition.state != CompetitionState.NOT_STARTED && ( <> - {competition.status} + {competition.state === CompetitionState.STARTED + ? "В прогрессе" + : "Завершено"} )}

- {competition.name} + {competition.title}

diff --git a/services/frontend/src/pages/Competitions/components/CompetitionTag/index.tsx b/services/frontend/src/pages/Competitions/components/CompetitionTag/index.tsx deleted file mode 100644 index 445688f..0000000 --- a/services/frontend/src/pages/Competitions/components/CompetitionTag/index.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import { cn } from "@/shared/lib/utils"; -import { Badge } from "@/components/ui/badge"; - -interface CompetitionTagProps { - label: string; - variant: 'olympics' | 'status'; - className?: string; -} - -const CompetitionTag = ({ label, variant, className }: CompetitionTagProps) => { - return ( - - {label} - - ); -} - -export default CompetitionTag \ No newline at end of file diff --git a/services/frontend/src/pages/Competitions/index.tsx b/services/frontend/src/pages/Competitions/index.tsx index cd09103..2506526 100644 --- a/services/frontend/src/pages/Competitions/index.tsx +++ b/services/frontend/src/pages/Competitions/index.tsx @@ -1,49 +1,96 @@ -import { useState } from "react"; -import { Competition, CompetitionStatus } from "@/shared/types"; -import { CompetitionGrid } from "./modules/CompetitionGrid"; +import React, { useState } from "react"; +import { CompetitionGrid } from "./modules/CompetitionsGrid"; import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"; -import { mockCompetitions } from "@/shared/mocks/mocks"; +import { useQuery } from "@tanstack/react-query"; +import { getCompetitions } from "@/shared/api/competitions"; +import { NoCompetitions } from "./modules/NoCompetitions"; +import { TabsContent } from "@radix-ui/react-tabs"; +import { Loading } from "@/components/ui/Loading"; +import { CompetitionState } from "@/shared/types/competition"; + +enum CompetitionTab { + ONGOING = "ongoing", + COMPLETED = "completed", +} const CompetitionsPage = () => { - const [competitions] = useState(mockCompetitions); - const [activeTab, setActiveTab] = useState("ongoing"); + const [activeTab, setActiveTab] = useState(CompetitionTab.ONGOING); - const myCompetitions = competitions.filter( - (comp) => - comp.status === CompetitionStatus.InProgress || - comp.status === CompetitionStatus.Completed, + const activeCompetitionsQuery = useQuery({ + queryKey: ["active-competitions"], + queryFn: async () => getCompetitions(true), + retry: 1, + }); + + const inactiveCompetitionsQuery = useQuery({ + queryKey: ["inactive-competitions"], + queryFn: async () => getCompetitions(false), + retry: 1, + }); + + const startedCompetitions = React.useMemo( + () => + (activeCompetitionsQuery.data ?? []).filter( + (comp) => comp.state === CompetitionState.STARTED, + ), + [activeCompetitionsQuery.data], ); - const filteredMyCompetitions = myCompetitions.filter((comp) => - activeTab === "ongoing" - ? comp.status === CompetitionStatus.InProgress - : comp.status === CompetitionStatus.Completed, + const finishedCompetitions = React.useMemo( + () => + (activeCompetitionsQuery.data ?? []).filter( + (comp) => comp.state === CompetitionState.FINISHED, + ), + [activeCompetitionsQuery.data], ); - const availableCompetitions = competitions.filter( - (comp) => comp.status === "Не участвую", - ); + if ( + activeCompetitionsQuery.isLoading || + inactiveCompetitionsQuery.isLoading + ) { + return ; + } return (
-
- - Мои события + {(activeCompetitionsQuery.data ?? []).length > 0 && ( +
- - В процессе - Завершенные - + + Мои события + + + + В процессе + + + Завершенные + + + + + + + + + + + - - -
+
+ )}
События - + {(inactiveCompetitionsQuery.data ?? []).length > 0 ? ( + + ) : ( + + )}
); diff --git a/services/frontend/src/pages/Competitions/modules/CompetitionGrid/index.tsx b/services/frontend/src/pages/Competitions/modules/CompetitionsGrid.tsx similarity index 80% rename from services/frontend/src/pages/Competitions/modules/CompetitionGrid/index.tsx rename to services/frontend/src/pages/Competitions/modules/CompetitionsGrid.tsx index 11d6289..60ac1fb 100644 --- a/services/frontend/src/pages/Competitions/modules/CompetitionGrid/index.tsx +++ b/services/frontend/src/pages/Competitions/modules/CompetitionsGrid.tsx @@ -1,5 +1,5 @@ -import { Competition } from "@/shared/types"; -import { CompetitionCard } from "../../components/CompetitionCard"; +import { Competition } from "@/shared/types/competition"; +import { CompetitionCard } from "../components/CompetitionCard"; import { Link } from "react-router"; interface CompetitionGridProps { diff --git a/services/frontend/src/pages/Competitions/modules/NoCompetitions.tsx b/services/frontend/src/pages/Competitions/modules/NoCompetitions.tsx new file mode 100644 index 0000000..8b71193 --- /dev/null +++ b/services/frontend/src/pages/Competitions/modules/NoCompetitions.tsx @@ -0,0 +1,15 @@ +import { Ban } from "lucide-react"; + +export const NoCompetitions = () => { + return ( +
+ +
+

Событий нет

+

+ Увы, очередная победа.рф +

+
+
+ ); +}; diff --git a/services/frontend/src/pages/Login/index.tsx b/services/frontend/src/pages/Login/index.tsx index 508b4ea..d7c4d88 100644 --- a/services/frontend/src/pages/Login/index.tsx +++ b/services/frontend/src/pages/Login/index.tsx @@ -18,7 +18,7 @@ const LoginPage = () => { return (
- +

Добро пожаловать! diff --git a/services/frontend/src/pages/Review/index.tsx b/services/frontend/src/pages/Review/index.tsx index e69de29..4d21663 100644 --- a/services/frontend/src/pages/Review/index.tsx +++ b/services/frontend/src/pages/Review/index.tsx @@ -0,0 +1,51 @@ +import { Loading } from "@/components/ui/Loading"; +import { getReviewer, getReviewerSubmissions } from "@/shared/api/review"; +import { useQuery } from "@tanstack/react-query"; +import { useNavigate, useParams } from "react-router"; +import { ReviewHeader } from "./modules/review-header"; +import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"; + +const ReviewPage = () => { + const { token } = useParams<{ token: string }>(); + const navigate = useNavigate(); + + const reviewerQuery = useQuery({ + queryKey: ["reviewer", token], + queryFn: async () => getReviewer(token || ""), + retry: 0, + }); + const submissionsQuery = useQuery({ + queryKey: ["submissions", token], + queryFn: async () => getReviewerSubmissions(token || ""), + retry: 0, + }); + + if (reviewerQuery.isLoading || submissionsQuery.isLoading) { + return ; + } + + if (!token || !reviewerQuery.data || !submissionsQuery.data) { + navigate("/"); + return; + } + + return ( +
+
+ + + +
+

Посылки

+ + Доступные + Проверенные + +
+
+
+
+ ); +}; + +export default ReviewPage; diff --git a/services/frontend/src/pages/Review/modules/review-header.tsx b/services/frontend/src/pages/Review/modules/review-header.tsx new file mode 100644 index 0000000..42782a6 --- /dev/null +++ b/services/frontend/src/pages/Review/modules/review-header.tsx @@ -0,0 +1,27 @@ +import { Button, buttonVariants } from "@/components/ui/button"; +import { DataRushReview } from "@/components/ui/icons/datarush-review"; +import { Reviewer } from "@/shared/types/review"; +import { Link } from "react-router"; + +interface ReviewHeaderProps { + reviewer: Reviewer; +} + +export const ReviewHeader = ({ reviewer }: ReviewHeaderProps) => { + return ( +
+ +
+

+ {reviewer.name} {reviewer.surname} +

+ + Выйти + +
+
+ ); +}; diff --git a/services/frontend/src/shared/api/auth.ts b/services/frontend/src/shared/api/auth.ts index 901a4e1..58e5c77 100644 --- a/services/frontend/src/shared/api/auth.ts +++ b/services/frontend/src/shared/api/auth.ts @@ -1,4 +1,4 @@ -import { authFetch } from "."; +import { apiFetch } from "."; interface AuthResponse { token: string; @@ -9,14 +9,14 @@ export const signup = async (body: { username: string; password: string; }) => { - return await authFetch("/sign-up", { + return await apiFetch("/sign-up", { method: "POST", body, }); }; export const login = async (body: { email: string; password: string }) => { - return await authFetch("/sign-in", { + return await apiFetch("/sign-in", { method: "POST", body, }); diff --git a/services/frontend/src/shared/api/competitions.ts b/services/frontend/src/shared/api/competitions.ts new file mode 100644 index 0000000..5a5eba0 --- /dev/null +++ b/services/frontend/src/shared/api/competitions.ts @@ -0,0 +1,14 @@ +import { userFetch } from "."; +import { Competition } from "../types/competition"; + +export const getCompetitions = async (participating?: boolean) => { + return await userFetch("/competitions", { + params: { + is_participating: participating, + }, + }); +}; + +export const getCompetition = async (id: string) => { + return await userFetch(`/competition/${id}`); +}; diff --git a/services/frontend/src/shared/api/index.ts b/services/frontend/src/shared/api/index.ts index 8772105..0616a21 100644 --- a/services/frontend/src/shared/api/index.ts +++ b/services/frontend/src/shared/api/index.ts @@ -14,17 +14,16 @@ export class ApiError extends Error { } } -export const authFetch = ofetch.create({ +export const apiFetch = ofetch.create({ baseURL: BASE_URL, async onResponseError({ response }) { throw new ApiError(response); }, }); -export const apiFetch = ofetch.create({ +export const userFetch = ofetch.create({ baseURL: BASE_URL, async onRequest({ options }) { - console.log(import.meta.env.VITE_API_ENDPOINT); options.headers.set("Authorization", "Bearer " + getToken()); }, async onResponseError({ response }) { diff --git a/services/frontend/src/shared/api/review.ts b/services/frontend/src/shared/api/review.ts new file mode 100644 index 0000000..887999b --- /dev/null +++ b/services/frontend/src/shared/api/review.ts @@ -0,0 +1,10 @@ +import { apiFetch } from "."; +import { Reviewer } from "../types/review"; + +export const getReviewer = async (token: string) => { + return await apiFetch(`/review/${token}`); +}; + +export const getReviewerSubmissions = async (token: string) => { + return await apiFetch(`/review/${token}/submissions`); +}; diff --git a/services/frontend/src/shared/api/user.ts b/services/frontend/src/shared/api/user.ts index b71c15f..84b000d 100644 --- a/services/frontend/src/shared/api/user.ts +++ b/services/frontend/src/shared/api/user.ts @@ -1,6 +1,6 @@ -import { apiFetch } from "."; +import { userFetch } from "."; import { User } from "../types/user"; export const getCurrentUser = async () => { - return await apiFetch("/me"); + return await userFetch("/me"); }; diff --git a/services/frontend/src/shared/types/competition.ts b/services/frontend/src/shared/types/competition.ts new file mode 100644 index 0000000..beea20e --- /dev/null +++ b/services/frontend/src/shared/types/competition.ts @@ -0,0 +1,26 @@ +export interface Competition { + id: string; + title: string; + description: string; + state: CompetitionState; + image_url?: string; + start_date?: Date; + end_date?: Date; + type: CompetitionType; + participation_type: CompetitionParticipationType; +} + +export enum CompetitionState { + NOT_STARTED = "not_started", + STARTED = "started", + FINISHED = "finished", +} + +export enum CompetitionType { + EDU = "edu", + COMPETITIVE = "competitive", +} + +export enum CompetitionParticipationType { + SOLO = "solo", +} diff --git a/services/frontend/src/shared/types/review.ts b/services/frontend/src/shared/types/review.ts new file mode 100644 index 0000000..f3a9094 --- /dev/null +++ b/services/frontend/src/shared/types/review.ts @@ -0,0 +1,5 @@ +export interface Reviewer { + id: string; + name: string; + surname: string; +} diff --git a/services/frontend/src/styles/globals.css b/services/frontend/src/styles/globals.css index 644cc81..860667b 100644 --- a/services/frontend/src/styles/globals.css +++ b/services/frontend/src/styles/globals.css @@ -88,7 +88,6 @@ --sidebar-accent-foreground: oklch(0.985 0 0); --sidebar-border: oklch(0.269 0 0); --sidebar-ring: oklch(0.439 0 0); - } @theme inline { @@ -120,6 +119,7 @@ --radius-md: calc(var(--radius) - 2px); --radius-lg: var(--radius); --radius-xl: calc(var(--radius) + 4px); + --radius-6: calc(var(--radius) + 6px); --color-sidebar: var(--sidebar); --color-sidebar-foreground: var(--sidebar-foreground); --color-sidebar-primary: var(--sidebar-primary); From 9d9f6c811fd0cb8071e6360fb1cfe61d28885a0a Mon Sep 17 00:00:00 2001 From: Timur Date: Sun, 2 Mar 2025 13:53:34 +0300 Subject: [PATCH 4/9] adad filter_horizontal to reviewers --- services/backend/apps/competition/admin.py | 1 + services/backend/apps/task/admin.py | 3 +++ services/backend/apps/task/models.py | 7 ++++++- 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/services/backend/apps/competition/admin.py b/services/backend/apps/competition/admin.py index 6c78551..a28901d 100644 --- a/services/backend/apps/competition/admin.py +++ b/services/backend/apps/competition/admin.py @@ -7,6 +7,7 @@ from apps.task.admin import CompetitionTaskInline @admin.register(Competition) class CompetitionAdmin(admin.ModelAdmin): list_display = ( + "id", "title", "end_date", "type", diff --git a/services/backend/apps/task/admin.py b/services/backend/apps/task/admin.py index dd3ed25..a09f852 100644 --- a/services/backend/apps/task/admin.py +++ b/services/backend/apps/task/admin.py @@ -12,6 +12,9 @@ class CompletionAttachmentInline(admin.StackedInline): @admin.register(CompetitionTask) class CompetitionTaskAdmin(admin.ModelAdmin): list_display = ("title", "type", "points") + filter_horizontal = ( + "reviewers", + ) @admin.register(CompetitionTaskSubmission) diff --git a/services/backend/apps/task/models.py b/services/backend/apps/task/models.py index b2741ed..3be2e97 100644 --- a/services/backend/apps/task/models.py +++ b/services/backend/apps/task/models.py @@ -51,7 +51,12 @@ class CompetitionTask(BaseModel): ) # only when "review" type - reviewers = models.ManyToManyField(Reviewer, blank=True) + reviewers = models.ManyToManyField( + Reviewer, + blank=True, + verbose_name="ревьюверы", + help_text="Справа отображаются действующие проверяющие, слева - доступные для выбора. Для перемещения можно кликнуть 2 раза по проверяющему" + ) def __str__(self): return self.title From bef7337d1c0c361de66d6d51c4ef80312ac3b513 Mon Sep 17 00:00:00 2001 From: Timur Date: Sun, 2 Mar 2025 13:53:57 +0300 Subject: [PATCH 5/9] remove reviews admin --- services/backend/apps/review/admin.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/services/backend/apps/review/admin.py b/services/backend/apps/review/admin.py index 48730de..c173af7 100644 --- a/services/backend/apps/review/admin.py +++ b/services/backend/apps/review/admin.py @@ -7,11 +7,3 @@ from apps.review.models import Review, Reviewer class ReviewersAdmin(admin.ModelAdmin): list_display = ("name", "surname",) search_fields = ("name", "surname",) - - -@admin.register(Review) -class ReviewAdmin(admin.ModelAdmin): - list_display = ("id", "reviewer", "submission",) - search_fields = ("id", "reviewer__id", "reviewer__name", "reviewer__surname", - "submission__id", "submission__content") - list_filter = ("submission__plagiarism_checked", "submission__status",) From 45f3f610e4bda0d8da3bafc044eb5ff855f16b1a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BD=D0=B4=D1=80=D0=B5=D0=B9=20=D0=A1=D1=83=D0=BC?= =?UTF-8?q?=D0=B8=D0=BD?= Date: Sun, 2 Mar 2025 13:54:48 +0300 Subject: [PATCH 6/9] feat: added task_position in reviewer responce --- services/backend/api/v1/review/schemas.py | 1 + 1 file changed, 1 insertion(+) diff --git a/services/backend/api/v1/review/schemas.py b/services/backend/api/v1/review/schemas.py index 3e47f62..1b79004 100644 --- a/services/backend/api/v1/review/schemas.py +++ b/services/backend/api/v1/review/schemas.py @@ -37,6 +37,7 @@ class SubmissionOut(ModelSchema): submitted_at: datetime = Field(..., alias="timestamp") competition: UUID = Field(..., alias="task.competition.id") competition_name: str = Field(..., alias="task.competition.title") + task_position: int = Field(..., alias="task.in_competition_position") @staticmethod def resolve_criteries(self, context) -> list[CriteriaOut] | None: From fc7f7bea66ff5a664003b7bf4e8121219b0ddbf3 Mon Sep 17 00:00:00 2001 From: Timur Date: Sun, 2 Mar 2025 13:57:42 +0300 Subject: [PATCH 7/9] add verbose names to submission admin --- services/backend/apps/task/models.py | 35 +++++++++++++++++++++------- 1 file changed, 26 insertions(+), 9 deletions(-) diff --git a/services/backend/apps/task/models.py b/services/backend/apps/task/models.py index 3be2e97..60e54c9 100644 --- a/services/backend/apps/task/models.py +++ b/services/backend/apps/task/models.py @@ -101,8 +101,10 @@ class CompetitionTaskSubmission(BaseModel): def submission_stdout_upload_to(instance, filename) -> str: return f"/submissions/{instance.id}/stdout" - user = models.ForeignKey(User, on_delete=models.CASCADE) - task = models.ForeignKey(CompetitionTask, on_delete=models.CASCADE) + user = models.ForeignKey(User, on_delete=models.CASCADE, + verbose_name="пользователь") + task = models.ForeignKey(CompetitionTask, on_delete=models.CASCADE, + verbose_name="задание") status = models.CharField( choices=StatusChoices.choices, @@ -112,24 +114,39 @@ class CompetitionTaskSubmission(BaseModel): ) # code or text or file - content = models.FileField(upload_to=submission_content_upload_to) + content = models.FileField(upload_to=submission_content_upload_to, + verbose_name="содержание посылки") # only if task type is checker stdout = models.FileField( - upload_to=submission_stdout_upload_to, null=True, blank=True + upload_to=submission_stdout_upload_to, null=True, blank=True, + verbose_name="вывод программы", + help_text="Используется только при проверке чекером" ) # depends on task type: # - input: {"correct": boolean} # - file: {"total_points": integer, "by_criteria": ["criteria_name": integer]} # - code: {"correct": boolean} - result = models.JSONField(default=None, null=True, blank=True) + result = models.JSONField(default=None, null=True, blank=True, + verbose_name="результат проверки") # just more readable result representation, maybe will be calcuated somehow more complex depends on criteria - earned_points = models.IntegerField(null=True, blank=True) + earned_points = models.IntegerField(null=True, blank=True, + verbose_name="баллы за задание") - checked_at = models.DateTimeField(null=True, blank=True) - plagiarism_checked = models.BooleanField(default=False) - timestamp = models.DateTimeField(auto_now_add=True) + checked_at = models.DateTimeField(null=True, blank=True, + verbose_name="дата проверки") + plagiarism_checked = models.BooleanField(default=False, + verbose_name="проверено на плагиат") + timestamp = models.DateTimeField(auto_now_add=True, + verbose_name="дата отправки") + + class Meta: + verbose_name = "посылка" + verbose_name_plural = "посылки" + + def __str__(self): + return str(self.id) def send_on_review(self): if not self.task.reviewers.exists(): From a2635740bbb93c5035d16cfc74f0618d31bb3341 Mon Sep 17 00:00:00 2001 From: Timur Date: Sun, 2 Mar 2025 13:58:07 +0300 Subject: [PATCH 8/9] change verbose name at users --- services/backend/apps/user/apps.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/backend/apps/user/apps.py b/services/backend/apps/user/apps.py index dd71f2d..e38650e 100644 --- a/services/backend/apps/user/apps.py +++ b/services/backend/apps/user/apps.py @@ -5,4 +5,4 @@ class UsersConfig(AppConfig): default_auto_field = "django.db.models.BigAutoField" name = "apps.user" label = "user" - verbose_name = "Пользователи" + verbose_name = "Пользователи (веб)" From f82cf67792202eae70aa3fc555b6bf134d868dc9 Mon Sep 17 00:00:00 2001 From: moolcoov Date: Sun, 2 Mar 2025 14:02:44 +0300 Subject: [PATCH 9/9] fix: loading --- services/frontend/src/pages/Competition/index.tsx | 2 +- services/frontend/src/pages/Competitions/index.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/services/frontend/src/pages/Competition/index.tsx b/services/frontend/src/pages/Competition/index.tsx index fa64ec5..b434fa8 100644 --- a/services/frontend/src/pages/Competition/index.tsx +++ b/services/frontend/src/pages/Competition/index.tsx @@ -5,7 +5,7 @@ import ReactMarkdown from "react-markdown"; import { mockTasks } from "@/shared/mocks/mocks"; import { useQuery } from "@tanstack/react-query"; import { getCompetition } from "@/shared/api/competitions"; -import { Loading } from "@/components/ui/Loading"; +import { Loading } from "@/components/ui/loading"; const CompetitionPage = () => { const { id } = useParams<{ id: string }>(); diff --git a/services/frontend/src/pages/Competitions/index.tsx b/services/frontend/src/pages/Competitions/index.tsx index 2506526..1acd5f0 100644 --- a/services/frontend/src/pages/Competitions/index.tsx +++ b/services/frontend/src/pages/Competitions/index.tsx @@ -5,7 +5,7 @@ import { useQuery } from "@tanstack/react-query"; import { getCompetitions } from "@/shared/api/competitions"; import { NoCompetitions } from "./modules/NoCompetitions"; import { TabsContent } from "@radix-ui/react-tabs"; -import { Loading } from "@/components/ui/Loading"; +import { Loading } from "@/components/ui/loading"; import { CompetitionState } from "@/shared/types/competition"; enum CompetitionTab {