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:
- apk add --no-cache openssh-client
script:
- echo $SSH_PRIVATE_KEY_BASE64
- mkdir -p ~/.ssh && chmod 700 ~/.ssh
- printf "%s" "$SSH_PRIVATE_KEY_BASE64" | base64 -d -i > ~/.ssh/id_rsa
- chmod 600 ~/.ssh/id_rsa
- ssh-keyscan -H "$SSH_HOST" >> ~/.ssh/known_hosts
- echo -e "Host *\n\tStrictHostKeyChecking no\n\n" > ~/.ssh/config && chmod 600 ~/.ssh/config
- echo "$SSH_PRIVATE_KEY_BASE64" | base64 -d > ~/.ssh/id_rsa && chmod 400 ~/.ssh/id_rsa
- 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/
- ssh "$SSH_ADDRESS" << 'EOF'
set -e
+25 -2
View File
@@ -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):
@@ -13,11 +14,33 @@ class CompetitionOut(ModelSchema):
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 = (
+34 -5
View File
@@ -2,11 +2,12 @@ from http import HTTPStatus as status
from uuid import UUID
from django.http import HttpRequest
from django.shortcuts import get_object_or_404
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 +19,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 +34,35 @@ 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)
+2
View File
@@ -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(),
)
+7 -3
View File
@@ -3,9 +3,14 @@ from http import HTTPStatus as status
from ninja import Router
from ninja.errors import AuthenticationError
from api.v1.user.schemas import LoginSchema, RegisterSchema, TokenSchema, UserSchema
from api.v1.auth import BearerAuth
from api.v1.schemas import BadRequestError, ForbiddenError, NotFoundError
from api.v1.user.schemas import (
LoginSchema,
RegisterSchema,
TokenSchema,
UserSchema,
)
from apps.user.models import User
router = Router(tags=["user"])
@@ -56,5 +61,4 @@ def sign_in(request, data: LoginSchema):
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 apps.core.models import BaseModel
from apps.user.models import User
class Competition(BaseModel):
class CompetitionType(models.TextChoices):
SOLO = "solo"
class CompetitionParticipationType(models.TextChoices):
EDU = "edu"
COMPETITIVE = "competitive"
class Competition(BaseModel):
title = models.CharField(max_length=100, verbose_name="Название")
description = models.TextField(verbose_name="Описание")
image_url = models.FileField(
@@ -34,7 +33,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)
-3
View File
@@ -1,7 +1,4 @@
import contextlib
from django.apps import AppConfig
from django.core.cache import cache
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
@@ -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="Юзернейм")
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