From 999f5b069016bc08c492cba6fece03deb1dfe823 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: Sat, 1 Mar 2025 02:33:06 +0300 Subject: [PATCH] feat: added competition logic --- .../backend/api/v1/competition/schemas.py | 24 ++++++++++- services/backend/api/v1/competition/views.py | 43 ++++++++++++++++--- services/backend/api/v1/router.py | 2 + .../0002_competition_participants.py | 19 ++++++++ .../apps/competition/migrations/0003_state.py | 28 ++++++++++++ services/backend/apps/competition/models.py | 32 +++++++++----- .../apps/user/migrations/0002_user_status.py | 18 ++++++++ .../user/migrations/0003_alter_user_status.py | 18 ++++++++ services/backend/apps/user/models.py | 2 +- 9 files changed, 167 insertions(+), 19 deletions(-) create mode 100644 services/backend/apps/competition/migrations/0002_competition_participants.py create mode 100644 services/backend/apps/competition/migrations/0003_state.py create mode 100644 services/backend/apps/user/migrations/0002_user_status.py create mode 100644 services/backend/apps/user/migrations/0003_alter_user_status.py diff --git a/services/backend/api/v1/competition/schemas.py b/services/backend/api/v1/competition/schemas.py index efd667a..47c86c1 100644 --- a/services/backend/api/v1/competition/schemas.py +++ b/services/backend/api/v1/competition/schemas.py @@ -1,8 +1,9 @@ +from typing import Literal from uuid import UUID -from ninja import ModelSchema +from ninja import ModelSchema, Schema -from apps.competition.models import Competition +from apps.competition.models import Competition, State class CompetitionOut(ModelSchema): @@ -12,12 +13,31 @@ class CompetitionOut(ModelSchema): model = Competition fields = "__all__" +class StateOut(ModelSchema): + class Meta: + model = State + fields = ( + "state", + ) + +class StateIn(Schema): + state: Literal["started", "not_started", "finished"] class CompetitionListInstanceOut(ModelSchema): id: UUID is_participating: bool completed: bool + @staticmethod + def resolve_is_participating(self, context): + user = context["request"].auth + return self.participants.filter(id=user.id).exists() + + @staticmethod + def resolve_completed(self, context): + user = context["request"].auth + return State.objects.filter(competition=self, user=user, state="finished").exists() + class Meta: model = Competition fields = ( diff --git a/services/backend/api/v1/competition/views.py b/services/backend/api/v1/competition/views.py index 95a70d5..81ebdaf 100644 --- a/services/backend/api/v1/competition/views.py +++ b/services/backend/api/v1/competition/views.py @@ -1,12 +1,14 @@ from http import HTTPStatus as status +from typing import Literal from uuid import UUID - -from django.http import HttpRequest +from django.shortcuts import get_object_or_404 +from django.http import HttpRequest, Http404 from ninja import Router import api.v1.schemas as global_schemas from api.v1.auth import BearerAuth from api.v1.competition import schemas +from apps.competition.models import Competition, State router = Router(tags=["competition"]) @@ -18,11 +20,12 @@ router = Router(tags=["competition"]) status.BAD_REQUEST: global_schemas.BadRequestError, status.UNAUTHORIZED: global_schemas.UnauthorizedError, }, - auth=BearerAuth(), ) def get_competition( request: HttpRequest, competition_id: UUID -) -> tuple[status, schemas.CompetitionOut]: ... +) -> tuple[status, schemas.CompetitionOut]: + competition = get_object_or_404(Competition, id=competition_id) + return status.OK, competition @router.get( @@ -32,8 +35,36 @@ def get_competition( status.BAD_REQUEST: global_schemas.BadRequestError, status.UNAUTHORIZED: global_schemas.UnauthorizedError, }, - auth=BearerAuth(), ) def list_competitions( request: HttpRequest, is_participating: bool -) -> tuple[status, list[schemas.CompetitionListInstanceOut]]: ... +) -> tuple[status, list[schemas.CompetitionListInstanceOut]]: + user = request.auth + if is_participating: + competitions = Competition.objects.filter(participants=user) + else: + competitions = Competition.objects.exclude(participants=user) + return status.OK, competitions + +@router.post( + "competitions/{competition_id}/state", + response={ + status.OK: schemas.StateOut, + status.BAD_REQUEST: global_schemas.BadRequestError, + status.UNAUTHORIZED: global_schemas.UnauthorizedError, + } +) +def change_competition_state( + request: HttpRequest, + competition_id: UUID, + state: schemas.StateIn, +) -> tuple[status, schemas.StateOut]: + user = request.auth + competition = get_object_or_404(Competition, id=competition_id) + + state_obj, _ = State.objects.update_or_create( + user=user, + competition=competition, + state=state.state + ) + return status.OK, schemas.StateOut.from_orm(state_obj) diff --git a/services/backend/api/v1/router.py b/services/backend/api/v1/router.py index 62d8ca4..848448f 100644 --- a/services/backend/api/v1/router.py +++ b/services/backend/api/v1/router.py @@ -3,6 +3,7 @@ from functools import partial from ninja import NinjaAPI from api.v1 import handlers +from api.v1.auth import BearerAuth from api.v1.competition.views import router as competition_router from api.v1.ping.views import router as ping_router from api.v1.user.views import router as user_router @@ -12,6 +13,7 @@ router = NinjaAPI( version="1", description="API docs for DataRush", openapi_url="/docs/openapi.json", + auth=BearerAuth() ) diff --git a/services/backend/apps/competition/migrations/0002_competition_participants.py b/services/backend/apps/competition/migrations/0002_competition_participants.py new file mode 100644 index 0000000..a15dafd --- /dev/null +++ b/services/backend/apps/competition/migrations/0002_competition_participants.py @@ -0,0 +1,19 @@ +# Generated by Django 5.1.6 on 2025-02-28 22:40 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('competition', '0001_initial'), + ('user', '0002_user_status'), + ] + + operations = [ + migrations.AddField( + model_name='competition', + name='participants', + field=models.ManyToManyField(related_name='participants', to='user.user'), + ), + ] diff --git a/services/backend/apps/competition/migrations/0003_state.py b/services/backend/apps/competition/migrations/0003_state.py new file mode 100644 index 0000000..2212552 --- /dev/null +++ b/services/backend/apps/competition/migrations/0003_state.py @@ -0,0 +1,28 @@ +# Generated by Django 5.1.6 on 2025-02-28 23:26 + +import django.db.models.deletion +import uuid +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('competition', '0002_competition_participants'), + ('user', '0003_alter_user_status'), + ] + + operations = [ + migrations.CreateModel( + name='State', + fields=[ + ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), + ('state', models.CharField(choices=[('not_started', 'Not Started'), ('started', 'Started'), ('finished', 'Finished')], max_length=11)), + ('competition', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='competition.competition')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='user.user')), + ], + options={ + 'abstract': False, + }, + ), + ] diff --git a/services/backend/apps/competition/models.py b/services/backend/apps/competition/models.py index 952058b..08538bd 100644 --- a/services/backend/apps/competition/models.py +++ b/services/backend/apps/competition/models.py @@ -1,18 +1,18 @@ from django.db import models from apps.core.models import BaseModel - - -class CompetitionType(models.TextChoices): - SOLO = "solo" - - -class CompetitionParticipationType(models.TextChoices): - EDU = "edu" - COMPETITIVE = "competitive" - +from apps.user.models import User class Competition(BaseModel): + class CompetitionType(models.TextChoices): + SOLO = "solo" + + + class CompetitionParticipationType(models.TextChoices): + EDU = "edu" + COMPETITIVE = "competitive" + + title = models.CharField(max_length=100, verbose_name="Название") description = models.TextField(verbose_name="Описание") image_url = models.FileField( @@ -34,7 +34,19 @@ class Competition(BaseModel): choices=CompetitionParticipationType.choices, verbose_name="Тип соревнования", ) + participants = models.ManyToManyField(User, related_name="participants") class Meta: verbose_name = "соревнование" verbose_name_plural = "соревнования" + + +class State(BaseModel): + class StateChoices(models.TextChoices): + NOT_STARTED = 'not_started' + STARTED = 'started' + FINISHED = 'finished' + + user = models.ForeignKey(User, on_delete=models.CASCADE) + competition = models.ForeignKey(Competition, on_delete=models.CASCADE) + state = models.CharField(choices=StateChoices.choices, max_length=11) \ No newline at end of file diff --git a/services/backend/apps/user/migrations/0002_user_status.py b/services/backend/apps/user/migrations/0002_user_status.py new file mode 100644 index 0000000..281d8fd --- /dev/null +++ b/services/backend/apps/user/migrations/0002_user_status.py @@ -0,0 +1,18 @@ +# Generated by Django 5.1.6 on 2025-02-28 22:40 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('user', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='user', + name='status', + field=models.CharField(choices=[('student', 'Student'), ('metodist', 'Metodist')], default='student', max_length=10), + ), + ] diff --git a/services/backend/apps/user/migrations/0003_alter_user_status.py b/services/backend/apps/user/migrations/0003_alter_user_status.py new file mode 100644 index 0000000..a7c766f --- /dev/null +++ b/services/backend/apps/user/migrations/0003_alter_user_status.py @@ -0,0 +1,18 @@ +# Generated by Django 5.1.6 on 2025-02-28 22:45 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('user', '0002_user_status'), + ] + + operations = [ + migrations.AlterField( + model_name='user', + name='status', + field=models.CharField(choices=[('student', 'Student'), ('metodist', 'Metodist')], default='student', max_length=10), + ), + ] diff --git a/services/backend/apps/user/models.py b/services/backend/apps/user/models.py index ab52004..be30d58 100644 --- a/services/backend/apps/user/models.py +++ b/services/backend/apps/user/models.py @@ -13,7 +13,7 @@ class User(BaseModel): username = models.SlugField(unique=True, verbose_name="Юзернейм") password = models.TextField(verbose_name="Пароль") - status = models.CharField(max_length=10, choices=UserRole.choices, default=UserRole.STUDENT) + status = models.CharField(max_length=10, choices=UserRole, default="student") def __str__(self): return self.username