Merge remote-tracking branch 'origin/master'

# Conflicts:
#	services/backend/apps/user/migrations/0002_user_status.py
This commit is contained in:
Timur
2025-03-01 02:36:21 +03:00
12 changed files with 163 additions and 27 deletions
+6 -3
View File
@@ -61,10 +61,13 @@ deploy:
before_script: before_script:
- apk add --no-cache openssh-client - apk add --no-cache openssh-client
script: script:
- echo $SSH_PRIVATE_KEY_BASE64
- mkdir -p ~/.ssh && chmod 700 ~/.ssh - mkdir -p ~/.ssh && chmod 700 ~/.ssh
- printf "%s" "$SSH_PRIVATE_KEY_BASE64" | base64 -d -i > ~/.ssh/id_rsa - echo -e "Host *\n\tStrictHostKeyChecking no\n\n" > ~/.ssh/config && chmod 600 ~/.ssh/config
- chmod 600 ~/.ssh/id_rsa - echo "$SSH_PRIVATE_KEY_BASE64" | base64 -d > ~/.ssh/id_rsa && chmod 400 ~/.ssh/id_rsa
- ssh-keyscan -H "$SSH_HOST" >> ~/.ssh/known_hosts - cat ~/.ssh/id_rsa
- ssh-agent sh -c "ssh-add ~/.ssh/id_rsa"
- ssh-keyscan -H "$SSH_HOST"
- scp -C -r infrastructure/ compose.yaml "$SSH_ADDRESS":~/deploy/ - scp -C -r infrastructure/ compose.yaml "$SSH_ADDRESS":~/deploy/
- ssh "$SSH_ADDRESS" << 'EOF' - ssh "$SSH_ADDRESS" << 'EOF'
set -e set -e
+25 -2
View File
@@ -1,8 +1,9 @@
from typing import Literal
from uuid import UUID 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): class CompetitionOut(ModelSchema):
@@ -13,11 +14,33 @@ class CompetitionOut(ModelSchema):
fields = "__all__" fields = "__all__"
class StateOut(ModelSchema):
class Meta:
model = State
fields = ("state",)
class StateIn(Schema):
state: Literal["started", "not_started", "finished"]
class CompetitionListInstanceOut(ModelSchema): class CompetitionListInstanceOut(ModelSchema):
id: UUID id: UUID
is_participating: bool is_participating: bool
completed: 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: class Meta:
model = Competition model = Competition
fields = ( fields = (
+34 -5
View File
@@ -2,11 +2,12 @@ from http import HTTPStatus as status
from uuid import UUID 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
import api.v1.schemas as global_schemas import api.v1.schemas as global_schemas
from api.v1.auth import BearerAuth
from api.v1.competition import schemas from api.v1.competition import schemas
from apps.competition.models import Competition, State
router = Router(tags=["competition"]) router = Router(tags=["competition"])
@@ -18,11 +19,12 @@ router = Router(tags=["competition"])
status.BAD_REQUEST: global_schemas.BadRequestError, status.BAD_REQUEST: global_schemas.BadRequestError,
status.UNAUTHORIZED: global_schemas.UnauthorizedError, status.UNAUTHORIZED: global_schemas.UnauthorizedError,
}, },
auth=BearerAuth(),
) )
def get_competition( def get_competition(
request: HttpRequest, competition_id: UUID 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( @router.get(
@@ -32,8 +34,35 @@ def get_competition(
status.BAD_REQUEST: global_schemas.BadRequestError, status.BAD_REQUEST: global_schemas.BadRequestError,
status.UNAUTHORIZED: global_schemas.UnauthorizedError, status.UNAUTHORIZED: global_schemas.UnauthorizedError,
}, },
auth=BearerAuth(),
) )
def list_competitions( def list_competitions(
request: HttpRequest, is_participating: bool 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)
+2
View File
@@ -3,6 +3,7 @@ from functools import partial
from ninja import NinjaAPI from ninja import NinjaAPI
from api.v1 import handlers from api.v1 import handlers
from api.v1.auth import BearerAuth
from api.v1.competition.views import router as competition_router from api.v1.competition.views import router as competition_router
from api.v1.ping.views import router as ping_router from api.v1.ping.views import router as ping_router
from api.v1.user.views import router as user_router from api.v1.user.views import router as user_router
@@ -12,6 +13,7 @@ router = NinjaAPI(
version="1", version="1",
description="API docs for DataRush", description="API docs for DataRush",
openapi_url="/docs/openapi.json", openapi_url="/docs/openapi.json",
auth=BearerAuth(),
) )
+7 -3
View File
@@ -3,9 +3,14 @@ from http import HTTPStatus as status
from ninja import Router from ninja import Router
from ninja.errors import AuthenticationError from ninja.errors import AuthenticationError
from api.v1.user.schemas import LoginSchema, RegisterSchema, TokenSchema, UserSchema
from api.v1.auth import BearerAuth from api.v1.auth import BearerAuth
from api.v1.schemas import BadRequestError, ForbiddenError, NotFoundError from api.v1.schemas import BadRequestError, ForbiddenError, NotFoundError
from api.v1.user.schemas import (
LoginSchema,
RegisterSchema,
TokenSchema,
UserSchema,
)
from apps.user.models import User from apps.user.models import User
router = Router(tags=["user"]) router = Router(tags=["user"])
@@ -56,5 +61,4 @@ def sign_in(request, data: LoginSchema):
status.NOT_FOUND: NotFoundError, status.NOT_FOUND: NotFoundError,
}, },
) )
def get_user(request, user_id: str): def get_user(request, user_id: str): ...
...
@@ -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'),
),
]
@@ -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,
},
),
]
+14 -3
View File
@@ -1,18 +1,17 @@
from django.db import models from django.db import models
from apps.core.models import BaseModel from apps.core.models import BaseModel
from apps.user.models import User
class Competition(BaseModel):
class CompetitionType(models.TextChoices): class CompetitionType(models.TextChoices):
SOLO = "solo" SOLO = "solo"
class CompetitionParticipationType(models.TextChoices): class CompetitionParticipationType(models.TextChoices):
EDU = "edu" EDU = "edu"
COMPETITIVE = "competitive" COMPETITIVE = "competitive"
class Competition(BaseModel):
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(
@@ -34,7 +33,19 @@ class Competition(BaseModel):
choices=CompetitionParticipationType.choices, choices=CompetitionParticipationType.choices,
verbose_name="Тип соревнования", verbose_name="Тип соревнования",
) )
participants = models.ManyToManyField(User, related_name="participants")
class Meta: class Meta:
verbose_name = "соревнование" verbose_name = "соревнование"
verbose_name_plural = "соревнования" 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)
-3
View File
@@ -1,7 +1,4 @@
import contextlib
from django.apps import AppConfig from django.apps import AppConfig
from django.core.cache import cache
class CoreConfig(AppConfig): class CoreConfig(AppConfig):
@@ -1,4 +1,4 @@
# Generated by Django 5.1.6 on 2025-02-28 21:40 # Generated by Django 5.1.6 on 2025-02-28 22:40
from django.db import migrations, models from django.db import migrations, models
@@ -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),
),
]
+3 -1
View File
@@ -13,7 +13,9 @@ class User(BaseModel):
username = models.SlugField(unique=True, verbose_name="Юзернейм") username = models.SlugField(unique=True, verbose_name="Юзернейм")
password = models.TextField(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): def __str__(self):
return self.username return self.username