This commit is contained in:
rngsurrounded
2025-03-03 17:11:22 +09:00
33 changed files with 176 additions and 69 deletions
+1 -2
View File
@@ -107,12 +107,12 @@ deploy:
- | - |
ssh $SSH_ADDRESS <<'EOF' ssh $SSH_ADDRESS <<'EOF'
cd ~/deploy cd ~/deploy
docker system prune -a --force
docker compose pull > deploy.log 2>&1 docker compose pull > deploy.log 2>&1
docker compose down >> 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 up -d --remove-orphans --force-recreate >> deploy.log 2>&1
docker compose ps >> deploy.log 2>&1 docker compose ps >> deploy.log 2>&1
EOF EOF
- ssh $SSH_ADDRESS "docker system prune -a --force"
retry: 2 retry: 2
@@ -146,5 +146,4 @@ reset-compose:
docker compose up -d --remove-orphans --force-recreate >> deploy.log 2>&1 docker compose up -d --remove-orphans --force-recreate >> deploy.log 2>&1
docker compose ps >> deploy.log 2>&1 docker compose ps >> deploy.log 2>&1
EOF EOF
- ssh $SSH_ADDRESS "docker system prune -a --force"
retry: 2 retry: 2
+9
View File
@@ -370,11 +370,20 @@ services:
restart: unless-stopped restart: unless-stopped
shm_size: 4mb shm_size: 4mb
custom_python:
image: gitlab.prodcontest.ru:5050/team-15/project/custom-python:latest
entrypoint: ["sh", "-c", "exit 0"]
checker: checker:
image: gitlab.prodcontest.ru:5050/team-15/project/checker:latest image: gitlab.prodcontest.ru:5050/team-15/project/checker:latest
build: build:
context: ./services/checker context: ./services/checker
dockerfile: Dockerfile dockerfile: Dockerfile
depends_on:
custom_python:
restart: false
condition: service_completed_successfully
required: true
env_file: env_file:
- path: ./infrastructure/checker/.env.template - path: ./infrastructure/checker/.env.template
required: true required: true
+13 -2
View File
@@ -1,6 +1,7 @@
from ninja import ModelSchema from ninja import ModelSchema, Schema
from pydantic import Field
from apps.achievement.models import Achievement from apps.achievement.models import Achievement, UserAchievement
class AchievementSchema(ModelSchema): class AchievementSchema(ModelSchema):
@@ -12,3 +13,13 @@ class AchievementSchema(ModelSchema):
"description", "description",
"icon", "icon",
) )
class UserAchievementSchema(Schema):
name: str = Field(..., alias="achievement.name")
description: str = Field(..., alias="achievement.description")
icon: str = Field(..., alias="achievement.icon")
class Meta:
model = UserAchievement
fields = ("received_at",)
+3 -2
View File
@@ -6,15 +6,16 @@ from api.v1.achievement.schemas import AchievementSchema
from api.v1.schemas import UnauthorizedError from api.v1.schemas import UnauthorizedError
from apps.achievement.models import Achievement from apps.achievement.models import Achievement
router = Router() router = Router(tags=["achievement"])
@router.get( @router.get(
"", "all",
response={ response={
status.OK: list[AchievementSchema], status.OK: list[AchievementSchema],
status.UNAUTHORIZED: UnauthorizedError, status.UNAUTHORIZED: UnauthorizedError,
}, },
auth=None,
) )
def get_all_achievements(request): def get_all_achievements(request):
return Achievement.objects.all() return Achievement.objects.all()
+6
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.achievement.views import router as achievement_router
from api.v1.auth import BearerAuth 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
@@ -49,6 +50,11 @@ router.add_router(
team_router, team_router,
auth=BearerAuth(), auth=BearerAuth(),
) )
router.add_router(
"achievements",
achievement_router,
auth=BearerAuth(),
)
for exception, handler in handlers.exception_handlers: for exception, handler in handlers.exception_handlers:
+6
View File
@@ -12,6 +12,7 @@ from api.v1.task.schemas import (
TaskOutSchema, TaskOutSchema,
TaskSubmissionOut, TaskSubmissionOut,
) )
from apps.achievement.models import Achievement, UserAchievement
from apps.competition.models import State from apps.competition.models import State
from apps.task.models import ( from apps.task.models import (
Competition, Competition,
@@ -102,6 +103,11 @@ def submit_task(
CompetitionTask, competition=competition, id=task_id CompetitionTask, competition=competition, id=task_id
) )
if not CompetitionTaskSubmission.objects.filter(user=user).exists():
first_steps_achievement = Achievement.objects.get(slug="first_steps")
UserAchievement.objects.create(
user=user, achievement=first_steps_achievement
)
if task.type == CompetitionTask.CompetitionTaskType.INPUT: if task.type == CompetitionTask.CompetitionTaskType.INPUT:
submission = CompetitionTaskSubmission.objects.create( submission = CompetitionTaskSubmission.objects.create(
user=user, user=user,
+11 -1
View File
@@ -1,5 +1,7 @@
from ninja import ModelSchema, Schema from ninja import ModelSchema, Schema
from api.v1.achievement.schemas import UserAchievementSchema
from apps.achievement.models import UserAchievement
from apps.user.models import User from apps.user.models import User
@@ -20,9 +22,17 @@ class LoginSchema(ModelSchema):
class UserSchema(ModelSchema): class UserSchema(ModelSchema):
achievements: list[UserAchievementSchema] = None
@staticmethod
def resolve_achievements(self, context):
return UserAchievement.objects.filter(
user=context.get("request").auth
).order_by("-received_at")
class Meta: class Meta:
model = User model = User
fields = ["id", "email", "username", "created_at", "achievements"] fields = ["id", "email", "username", "created_at"]
class StatSchema(Schema): class StatSchema(Schema):
+9 -10
View File
@@ -11,17 +11,18 @@ from api.v1.schemas import (
BadRequestError, BadRequestError,
ConflictError, ConflictError,
ForbiddenError, ForbiddenError,
NotFoundError, UnauthorizedError, NotFoundError,
UnauthorizedError,
) )
from api.v1.user.schemas import ( from api.v1.user.schemas import (
LoginSchema, LoginSchema,
RegisterSchema, RegisterSchema,
StatSchema,
TokenSchema, TokenSchema,
UserSchema, UserSchema,
StatSchema
) )
from apps.task.models import CompetitionTaskSubmission
from apps.user.models import User from apps.user.models import User
from apps.task.models import CompetitionTaskSubmission, ReviewStatusChoices
router = Router(tags=["user"]) router = Router(tags=["user"])
@@ -91,16 +92,15 @@ def get_user(request, user_id: str):
@router.get( @router.get(
"/me/stat", "/me/stat",
response={ response={status.OK: StatSchema, status.UNAUTHORIZED: UnauthorizedError},
status.OK: StatSchema,
status.UNAUTHORIZED: UnauthorizedError
},
) )
def get_my_stat(request): def get_my_stat(request):
user_submissions = CompetitionTaskSubmission.objects.filter( user_submissions = CompetitionTaskSubmission.objects.filter(
user=request.auth user=request.auth
) )
checked_attempts = user_submissions.filter(status=CompetitionTaskSubmission.StatusChoices.CHECKED).all() checked_attempts = user_submissions.filter(
status=CompetitionTaskSubmission.StatusChoices.CHECKED
).all()
success_attempts_cnt = 0 success_attempts_cnt = 0
for attempt in checked_attempts: for attempt in checked_attempts:
@@ -112,6 +112,5 @@ def get_my_stat(request):
success_attempts_cnt += 1 success_attempts_cnt += 1
return StatSchema( return StatSchema(
total_attempts=len(user_submissions), total_attempts=len(user_submissions), solved_tasks=success_attempts_cnt
solved_tasks=success_attempts_cnt
) )
Binary file not shown.

After

Width:  |  Height:  |  Size: 182 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 193 KiB

@@ -0,0 +1,32 @@
from django.conf import settings
from django.core.files import File
from django.core.management import BaseCommand
from apps.achievement.models import Achievement
icons_dir = f"{settings.BASE_DIR}/apps/achievement/icons"
class Command(BaseCommand):
help = "Create achievement fixtures."
def handle(self, *args, **options):
if not Achievement.objects.filter(slug="first_steps").exists():
with open(f"{icons_dir}/first_steps.png", "rb") as f:
first_steps_icon = File(f, name="first_steps.png")
Achievement.objects.get_or_create(
name="Первые шаги",
description="Отправьте свое первое решение на задачу!",
slug="first_steps",
icon=first_steps_icon,
)
if not Achievement.objects.filter(slug="welcome").exists():
with open(f"{icons_dir}/welcome.png", "rb") as f:
welcome_icon = File(f, name="welcome.png")
Achievement.objects.get_or_create(
name="Добро пожаловать!",
description="Зарегистрируйтесь на платформе",
slug="welcome",
icon=welcome_icon,
)
@@ -1,6 +1,7 @@
# Generated by Django 5.1.6 on 2025-03-02 21:24 # Generated by Django 5.1.6 on 2025-03-03 07:20
import apps.achievement.models import apps.achievement.models
import django.db.models.deletion
import uuid import uuid
from django.db import migrations, models from django.db import migrations, models
@@ -27,4 +28,15 @@ class Migration(migrations.Migration):
'verbose_name_plural': 'ачивки', 'verbose_name_plural': 'ачивки',
}, },
), ),
migrations.CreateModel(
name='UserAchievement',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('received_at', models.DateTimeField(auto_now_add=True)),
('achievement', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='achievement.achievement')),
],
options={
'abstract': False,
},
),
] ]
@@ -0,0 +1,22 @@
# Generated by Django 5.1.6 on 2025-03-03 07:20
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
('achievement', '0001_initial'),
('user', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='userachievement',
name='user',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='user.user'),
),
]
+7 -3
View File
@@ -4,9 +4,6 @@ from apps.core.models import BaseModel
class Achievement(BaseModel): class Achievement(BaseModel):
class AchievementType(models.TextChoices):
CORRECT_TASKS = "correct_tasks", "Выполненные задания"
def image_url_upload_to(instance, filename): def image_url_upload_to(instance, filename):
return f"achievements/{instance.id}/icon/{filename}" return f"achievements/{instance.id}/icon/{filename}"
@@ -27,3 +24,10 @@ class Achievement(BaseModel):
class Meta: class Meta:
verbose_name = "ачивка" verbose_name = "ачивка"
verbose_name_plural = "ачивки" verbose_name_plural = "ачивки"
class UserAchievement(BaseModel):
achievement = models.ForeignKey(Achievement, on_delete=models.CASCADE)
user = models.ForeignKey("user.User", on_delete=models.CASCADE)
received_at = models.DateTimeField(auto_now_add=True)
@@ -1,4 +1,4 @@
# Generated by Django 5.1.6 on 2025-03-02 21:24 # Generated by Django 5.1.6 on 2025-03-03 07:20
import apps.competition.models import apps.competition.models
import datetime import datetime
@@ -9,7 +9,11 @@ from django.utils import timezone
from apps.competition.models import Competition, State from apps.competition.models import Competition, State
from apps.review.models import Reviewer from apps.review.models import Reviewer
from apps.task.models import CompetitionTask, CompetitionTaskSubmission, CompetitionTaskCriteria from apps.task.models import (
CompetitionTask,
CompetitionTaskCriteria,
CompetitionTaskSubmission,
)
from apps.user.models import User, UserRole from apps.user.models import User, UserRole
@@ -92,7 +96,7 @@ class Command(BaseCommand):
task_types = [ task_types = [
CompetitionTask.CompetitionTaskType.INPUT.value, CompetitionTask.CompetitionTaskType.INPUT.value,
CompetitionTask.CompetitionTaskType.REVIEW.value, CompetitionTask.CompetitionTaskType.REVIEW.value,
CompetitionTask.CompetitionTaskType.INPUT.value CompetitionTask.CompetitionTaskType.INPUT.value,
] ]
for comp in competitions: for comp in competitions:
# Create 3 tasks per competition # Create 3 tasks per competition
@@ -110,7 +114,10 @@ class Command(BaseCommand):
submission_reviewers_count=random.randint(2, 10), submission_reviewers_count=random.randint(2, 10),
max_attempts=random.randint(1, 10), max_attempts=random.randint(1, 10),
) )
if task_type == CompetitionTask.CompetitionTaskType.REVIEW.value: if (
task_type
== CompetitionTask.CompetitionTaskType.REVIEW.value
):
for j in range(5): for j in range(5):
CompetitionTaskCriteria.objects.create( CompetitionTaskCriteria.objects.create(
task=task, task=task,
+1 -1
View File
@@ -7,4 +7,4 @@ class CoreConfig(AppConfig):
verbose_name = "Проверка" verbose_name = "Проверка"
def ready(self): def ready(self):
import apps.review.signals pass
@@ -1,4 +1,4 @@
# Generated by Django 5.1.6 on 2025-03-02 21:24 # Generated by Django 5.1.6 on 2025-03-03 07:20
import uuid import uuid
from django.db import migrations, models from django.db import migrations, models
@@ -1,4 +1,4 @@
# Generated by Django 5.1.6 on 2025-03-02 21:24 # Generated by Django 5.1.6 on 2025-03-03 07:20
import django.db.models.deletion import django.db.models.deletion
from django.db import migrations, models from django.db import migrations, models
+2 -3
View File
@@ -2,13 +2,12 @@
from django.db.models.signals import m2m_changed from django.db.models.signals import m2m_changed
from django.dispatch import receiver from django.dispatch import receiver
from apps.review.models import Review
from apps.task.models import CompetitionTask, CompetitionTaskSubmission from apps.task.models import CompetitionTask, CompetitionTaskSubmission
@receiver(m2m_changed, sender=CompetitionTask.reviewers.through) @receiver(m2m_changed, sender=CompetitionTask.reviewers.through)
def print_reviewers(sender, instance, action, **kwargs): def print_reviewers(sender, instance, action, **kwargs):
if action in ['post_add', 'post_remove', 'post_clear']: if action in ["post_add", "post_remove", "post_clear"]:
submissions = CompetitionTaskSubmission.objects.filter(task=instance) submissions = CompetitionTaskSubmission.objects.filter(task=instance)
for submission in submissions: for submission in submissions:
submission.send_on_review() submission.send_on_review()
+1 -1
View File
@@ -33,7 +33,7 @@ class CompetitionTaskSubmissionAdmin(admin.ModelAdmin):
"user__username", "user__username",
"user__email", "user__email",
) )
list_filter = ("plagiarism_checked", "status",) list_filter = ("plagiarism_checked", "status")
ordering = ["-timestamp"] ordering = ["-timestamp"]
def has_add_permission(self, request, obj=None): def has_add_permission(self, request, obj=None):
@@ -1,4 +1,4 @@
# Generated by Django 5.1.6 on 2025-03-02 21:24 # Generated by Django 5.1.6 on 2025-03-03 07:20
import apps.task.models import apps.task.models
import django.db.models.deletion import django.db.models.deletion
+2 -3
View File
@@ -1,8 +1,7 @@
import httpx import httpx
from celery import shared_task from celery import shared_task
from django.core.files.base import ContentFile
from django.conf import settings from django.conf import settings
from django.core.files.base import ContentFile
@shared_task(bind=True, max_retries=3) @shared_task(bind=True, max_retries=3)
@@ -40,7 +39,7 @@ def analyze_data_task(self, submission_id):
) )
submission.status = CompetitionTaskSubmission.StatusChoices.CHECKED submission.status = CompetitionTaskSubmission.StatusChoices.CHECKED
except httpx.RequestError as e: except httpx.RequestError:
self.retry(countdown=2**self.request.retries) self.retry(countdown=2**self.request.retries)
except Exception as e: except Exception as e:
submission.result = {"error": str(e), "success": False} submission.result = {"error": str(e), "success": False}
@@ -1,4 +1,4 @@
# Generated by Django 5.1.6 on 2025-03-02 21:24 # Generated by Django 5.1.6 on 2025-03-03 07:20
import django.db.models.deletion import django.db.models.deletion
import uuid import uuid
+3
View File
@@ -6,3 +6,6 @@ class UsersConfig(AppConfig):
name = "apps.user" name = "apps.user"
label = "user" label = "user"
verbose_name = "Пользователи (веб)" verbose_name = "Пользователи (веб)"
def ready(self):
pass
@@ -1,4 +1,4 @@
# Generated by Django 5.1.6 on 2025-03-02 21:24 # Generated by Django 5.1.6 on 2025-03-03 07:20
import uuid import uuid
from django.db import migrations, models from django.db import migrations, models
+14
View File
@@ -0,0 +1,14 @@
from django.db.models.signals import post_save
from django.dispatch import receiver
from apps.achievement.models import Achievement, UserAchievement
from apps.user.models import User
@receiver(post_save, sender=User)
def assign_welcome_achievement(sender, instance, created, **kwargs):
if created:
welcome_achievement = Achievement.objects.get(slug="welcome")
UserAchievement.objects.create(
user=instance, achievement=welcome_achievement
)
+2 -1
View File
@@ -7,8 +7,9 @@ from pathlib import Path
import django_stubs_ext import django_stubs_ext
import environ import environ
from health_check.plugins import plugin_dir
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from health_check.plugins import plugin_dir
from integrations.checker.healthcheck import CheckerHealthCheck from integrations.checker.healthcheck import CheckerHealthCheck
BASE_DIR = Path(__file__).resolve().parent.parent BASE_DIR = Path(__file__).resolve().parent.parent
+2
View File
@@ -9,3 +9,5 @@ fi
if [ "$DJANGO_CREATE_SUPERUSER" = "True" ]; then if [ "$DJANGO_CREATE_SUPERUSER" = "True" ]; then
python manage.py createsuperuser --noinput --username "$DJANGO_SUPERUSER_USERNAME" --email "$DJANGO_SUPERUSER_EMAIL" || true python manage.py createsuperuser --noinput --username "$DJANGO_SUPERUSER_USERNAME" --email "$DJANGO_SUPERUSER_EMAIL" || true
fi fi
python manage.py init_achievments
-6
View File
@@ -1,10 +1,4 @@
# Change all vars before going to production and remove all comments (!) # Change all vars before going to production and remove all comments (!)
# Below all environment variables and default values # Below all environment variables and default values
REGISTRY_USERNAME=
REGISTRY_PASSWORD=
REGISTRY_URL=gitlab.prodcontest.ru:5050
DOCKER_IMAGE=gitlab.prodcontest.ru:5050/team-15/project/custom-python DOCKER_IMAGE=gitlab.prodcontest.ru:5050/team-15/project/custom-python
+1 -1
View File
@@ -30,7 +30,7 @@ ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONOPTIMIZE=2 \ PYTHONOPTIMIZE=2 \
PATH="/opt/venv/bin:$PATH" PATH="/opt/venv/bin:$PATH"
EXPOSE 8080 EXPOSE 8000
HEALTHCHECK --interval=30s --timeout=5s --start-period=5s --start-interval=2s --retries=3 \ 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 CMD wget --no-verbose --tries=1 --spider http://127.0.0.1:8000/health || exit 1
-6
View File
@@ -7,12 +7,6 @@ BASE_DIR = Path(__file__).resolve().parent
load_dotenv(BASE_DIR / ".env") load_dotenv(BASE_DIR / ".env")
REGISTRY_LOGIN = os.getenv("REGISTRY_USERNAME", None)
REGISTRY_PASSWORD = os.getenv("REGISTRY_USERNAME", None)
REGISTRY_URL = os.getenv("REGISTRY_URL", "gitlab.prodcontest.ru:5050")
DOCKER_IMAGE = os.getenv( DOCKER_IMAGE = os.getenv(
"DOCKER_IMAGE", default="gitlab.prodcontest.ru:5050/team-15/project/custom-python" "DOCKER_IMAGE", default="gitlab.prodcontest.ru:5050/team-15/project/custom-python"
) )
-17
View File
@@ -1,4 +1,3 @@
import docker.errors
from fastapi import FastAPI, HTTPException, status from fastapi import FastAPI, HTTPException, status
from pydantic import BaseModel, Field, HttpUrl from pydantic import BaseModel, Field, HttpUrl
import aiohttp import aiohttp
@@ -21,26 +20,10 @@ ALLOWED_FILENAME_CHARS = r"[^a-zA-Z0-9_\-.]"
app = FastAPI() app = FastAPI()
docker_client = docker.from_env() docker_client = docker.from_env()
print(docker_client.login(
username=config.REGISTRY_LOGIN,
password=config.REGISTRY_PASSWORD,
registry=config.REGISTRY_URL,
))
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
logging.basicConfig(level=logging.INFO) logging.basicConfig(level=logging.INFO)
@app.on_event("startup")
async def pull_docker_image():
client = docker.from_env()
image_name = config.DOCKER_IMAGE
try:
client.images.pull(image_name)
print(f"Successfully pulled {image_name}")
except docker.errors.DockerException as e:
print(f"Error pulling {image_name}: {e}")
class FileDetails(BaseModel): class FileDetails(BaseModel):
url: HttpUrl = Field( url: HttpUrl = Field(
..., description="URL to download the file from (supports HTTP/HTTPS)" ..., description="URL to download the file from (supports HTTP/HTTPS)"