This commit is contained in:
rngsurrounded
2025-03-02 22:49:09 +09:00
19 changed files with 180 additions and 106 deletions
@@ -1,4 +1,4 @@
from ninja import ModelSchema, Schema
from ninja import ModelSchema
from apps.achievement.models import Achievement
@@ -6,4 +6,9 @@ from apps.achievement.models import Achievement
class AchievementSchema(ModelSchema):
class Meta:
model = Achievement
fields = ("id", "name", "description", "icon",)
fields = (
"id",
"name",
"description",
"icon",
)
+2 -2
View File
@@ -2,9 +2,9 @@ from http import HTTPStatus as status
from ninja import Router
from apps.achievement.models import Achievement
from api.v1.achievement.schemas import AchievementSchema
from api.v1.schemas import UnauthorizedError
from apps.achievement.models import Achievement
router = Router()
@@ -14,7 +14,7 @@ router = Router()
response={
status.OK: list[AchievementSchema],
status.UNAUTHORIZED: UnauthorizedError,
}
},
)
def get_all_achievements(request):
return Achievement.objects.all()
+4 -4
View File
@@ -87,7 +87,9 @@ def evaluate_submission(
review.submission.checked_at = datetime.now()
review.save()
submission_evaluations = Review.objects.filter(submission=submission).values_list('evaluation', flat=True)
submission_evaluations = Review.objects.filter(
submission=submission
).values_list("evaluation", flat=True)
marks = []
for evaluation in submission_evaluations:
@@ -97,9 +99,7 @@ def evaluate_submission(
marks.append(mark)
earned_points = median(marks)
review.submission.earned_points = (
earned_points
)
review.submission.earned_points = earned_points
all_checked = not submission.reviews.exclude(
state=ReviewStatusChoices.CHECKED
+22 -6
View File
@@ -2,17 +2,24 @@ from typing import Literal
from uuid import UUID
from ninja import ModelSchema, Schema
from pydantic import Field
from apps.task.models import CompetitionTask, CompetitionTaskSubmission, CompetitionTaskAttachment
from apps.task.models import (
CompetitionTask,
CompetitionTaskAttachment,
CompetitionTaskSubmission,
)
class TaskOutSchema(ModelSchema):
status: Literal["sent", "checked", "checking", "not_submitted"] = None
@staticmethod
def resolve_status(self, context) -> Literal["sent", "checked", "checking", "not_submitted"]:
if submission := CompetitionTaskSubmission.objects.filter(task=self, user=context.get("request").auth).first():
def resolve_status(
self, context
) -> Literal["sent", "checked", "checking", "not_submitted"]:
if submission := CompetitionTaskSubmission.objects.filter(
task=self, user=context.get("request").auth
).first():
return submission.status
return "not_submitted"
@@ -38,10 +45,19 @@ class HistorySubmissionOut(ModelSchema):
class Meta:
model = CompetitionTaskSubmission
fields = ("id", "earned_points", "timestamp", "content",)
fields = (
"id",
"earned_points",
"timestamp",
"content",
)
class TaskAttachmentSchema(ModelSchema):
class Meta:
model = CompetitionTaskAttachment
fields = ("id", "file", "public",)
fields = (
"id",
"file",
"public",
)
+8 -2
View File
@@ -5,5 +5,11 @@ from apps.achievement.models import Achievement
@admin.register(Achievement)
class AchievementAdmin(admin.ModelAdmin):
list_display = ("id", "name",)
search_fields = ("name", "description",)
list_display = (
"id",
"name",
)
search_fields = (
"name",
"description",
)
+2 -2
View File
@@ -2,6 +2,6 @@ from django.apps import AppConfig
class AchievementConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'apps.achievement'
default_auto_field = "django.db.models.BigAutoField"
name = "apps.achievement"
verbose_name = "Ачивки"
+6 -6
View File
@@ -2,24 +2,24 @@ from django.db import models
from apps.core.models import BaseModel
class Achievement(BaseModel):
class AchievementType(models.TextChoices):
CORRECT_TASKS = "correct_tasks", "Выполненные задания"
def image_url_upload_to(instance, filename):
return f"/achievements/{instance.id}/icon"
return f"achievements/{instance.id}/icon/{filename}"
name = models.CharField(max_length=30, verbose_name="название",
unique=True)
name = models.CharField(
max_length=30, verbose_name="название", unique=True
)
description = models.TextField(verbose_name="описание")
icon = models.FileField(
verbose_name="иконка достижения",
upload_to=image_url_upload_to,
)
slug = models.SlugField(
verbose_name="слаг", unique=True
)
slug = models.SlugField(verbose_name="слаг", unique=True)
def __str__(self):
return self.name
+1 -1
View File
@@ -15,7 +15,7 @@ class Competition(BaseModel):
SOLO = "solo", "Индивидуальный"
def image_url_upload_to(instance, filename):
return f"/competitions/{instance.id}/image"
return f"competitions/{instance.id}/image/{filename}"
title = models.CharField(max_length=100, verbose_name="название")
description = models.TextField(verbose_name="описание")
+1 -1
View File
@@ -1,4 +1,4 @@
from django.contrib import admin
from django.contrib.auth.models import Group, User
from django.contrib.auth.models import Group
admin.site.unregister(Group)
@@ -8,7 +8,7 @@ from django.core.management.base import BaseCommand
from django.utils import timezone
from apps.competition.models import Competition, State
from apps.review.models import Review, Reviewer
from apps.review.models import Reviewer
from apps.task.models import CompetitionTask, CompetitionTaskSubmission
from apps.user.models import User, UserRole
+9 -3
View File
@@ -1,9 +1,15 @@
from django.contrib import admin
from apps.review.models import Review, Reviewer
from apps.review.models import Reviewer
@admin.register(Reviewer)
class ReviewersAdmin(admin.ModelAdmin):
list_display = ("name", "surname",)
search_fields = ("name", "surname",)
list_display = (
"name",
"surname",
)
search_fields = (
"name",
"surname",
)
+8 -6
View File
@@ -24,22 +24,24 @@ class ReviewStatusChoices(models.TextChoices):
class Review(BaseModel):
reviewer = models.ForeignKey(Reviewer, on_delete=models.CASCADE,
verbose_name="проверяющий")
reviewer = models.ForeignKey(
Reviewer, on_delete=models.CASCADE, verbose_name="проверяющий"
)
submission = models.ForeignKey(
"task.CompetitionTaskSubmission",
on_delete=models.CASCADE,
related_name="reviews",
verbose_name="посылка"
verbose_name="посылка",
)
evaluation = models.JSONField(default=list, null=True, blank=True,
verbose_name="выполнение")
evaluation = models.JSONField(
default=list, null=True, blank=True, verbose_name="выполнение"
)
state = models.CharField(
choices=ReviewStatusChoices.choices,
default=ReviewStatusChoices.NOT_CHECKED.value,
max_length=11,
verbose_name="состояние"
verbose_name="состояние",
)
def __str__(self):
+17 -7
View File
@@ -1,7 +1,10 @@
from django.contrib import admin
from apps.task.models import CompetitionTask, CompetitionTaskAttachment, \
CompetitionTaskSubmission
from apps.task.models import (
CompetitionTask,
CompetitionTaskAttachment,
CompetitionTaskSubmission,
)
class CompletionAttachmentInline(admin.StackedInline):
@@ -12,15 +15,22 @@ class CompletionAttachmentInline(admin.StackedInline):
@admin.register(CompetitionTask)
class CompetitionTaskAdmin(admin.ModelAdmin):
list_display = ("title", "type", "points")
filter_horizontal = (
"reviewers",
)
filter_horizontal = ("reviewers",)
@admin.register(CompetitionTaskSubmission)
class CompetitionTaskSubmissionAdmin(admin.ModelAdmin):
list_display = ("task", "user", "status",)
search_fields = ("task__id", "task__title", "user__username", "user__email")
list_display = (
"task",
"user",
"status",
)
search_fields = (
"task__id",
"task__title",
"user__username",
"user__email",
)
filter = ("plagiarism_checked",)
ordering = ["-timestamp"]
+55 -41
View File
@@ -2,12 +2,11 @@ from uuid import uuid4
from django.db import models
from django.db.models import Count, Q
from tinymce.models import HTMLField
from martor.models import MartorField
from apps.competition.models import Competition
from apps.core.models import BaseModel
from apps.review.models import Review, ReviewStatusChoices, Reviewer
from apps.review.models import Review, Reviewer, ReviewStatusChoices
from apps.user.models import User
@@ -18,7 +17,7 @@ class CompetitionTask(BaseModel):
REVIEW = "review", "Ручная"
def answer_file_upload_to(instance, filename) -> str:
return f"/tasks/{instance.id}/answer/{uuid4()}/filename"
return f"tasks/{instance.id}/answer/{uuid4()}/{filename}"
in_competition_position = models.PositiveSmallIntegerField(
null=True, blank=True
@@ -56,9 +55,11 @@ class CompetitionTask(BaseModel):
Reviewer,
blank=True,
verbose_name="ревьюверы",
help_text="Справа отображаются действующие проверяющие, слева - доступные для выбора. Для перемещения можно кликнуть 2 раза по проверяющему"
help_text="Справа отображаются действующие проверяющие, слева - доступные для выбора. Для перемещения можно кликнуть 2 раза по проверяющему",
)
submission_reviewers_count = models.PositiveSmallIntegerField(
default=1, null=True, blank=True
)
submission_reviewers_count = models.PositiveSmallIntegerField(default=1, null=True, blank=True)
def __str__(self):
return self.title
@@ -81,12 +82,12 @@ class CompetitionTaskCriteria(BaseModel):
class CompetitionTaskAttachment(BaseModel):
def file_upload_at(instance, filename):
return f"/attachment/{instance.id}/file"
return f"attachment/{instance.id}/file/{filename}"
task = models.ForeignKey(CompetitionTask, on_delete=models.CASCADE,
verbose_name="задание")
file = models.FileField(upload_to=file_upload_at,
verbose_name="файл")
task = models.ForeignKey(
CompetitionTask, on_delete=models.CASCADE, verbose_name="задание"
)
file = models.FileField(upload_to=file_upload_at, verbose_name="файл")
bind_at = models.FilePathField(verbose_name="путь сохранения")
public = models.BooleanField(default=False, verbose_name="публичный")
@@ -98,50 +99,61 @@ class CompetitionTaskSubmission(BaseModel):
CHECKED = "checked"
def submission_content_upload_to(instance, filename) -> str:
return f"submissions/{instance.id}/content"
return f"submissions/{instance.id}/content/{filename}"
def submission_stdout_upload_to(instance, filename) -> str:
return f"/submissions/{instance.id}/stdout"
return f"submissions/{instance.id}/stdout/{filename}"
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, verbose_name="пользователь"
)
task = models.ForeignKey(
CompetitionTask, on_delete=models.CASCADE, verbose_name="задание"
)
status = models.CharField(
choices=StatusChoices.choices,
default=StatusChoices.SENT,
max_length=8,
verbose_name="статус"
verbose_name="статус",
)
# 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,
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="Используется только при проверке чекером"
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,
verbose_name="результат проверки")
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,
verbose_name="баллы за задание")
earned_points = models.IntegerField(
null=True, blank=True, verbose_name="баллы за задание"
)
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="дата отправки")
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 = "посылка"
@@ -156,16 +168,18 @@ class CompetitionTaskSubmission(BaseModel):
reviewers_count = self.task.submission_reviewers_count
reviewers = self.task.reviewers.annotate(
pending_count=Count(
"review",
filter=Q(
review__state__in=[
ReviewStatusChoices.NOT_CHECKED,
ReviewStatusChoices.CHECKING,
]
),
)
).order_by("pending_count")[:reviewers_count] # да это медленно работает и чо
pending_count=Count(
"review",
filter=Q(
review__state__in=[
ReviewStatusChoices.NOT_CHECKED,
ReviewStatusChoices.CHECKING,
]
),
)
).order_by("pending_count")[
:reviewers_count
] # да это медленно работает и чо
for reviewer in reviewers:
Review.objects.create(
+10 -3
View File
@@ -6,8 +6,8 @@ import sys
import tempfile
from io import StringIO
from config.celery import app
from apps.task.models import CompetitionTaskSubmission
from config.celery import app
ALLOWED_MODULES = {
"pandas",
@@ -119,7 +119,12 @@ def secure_exec(code_str, result_path, input_files=None):
@app.task(bind=True)
def analyze_data_task(
self, code_str, result_path, expected_file_link, submission_id, input_files=[]
self,
code_str,
result_path,
expected_file_link,
submission_id,
input_files=[],
):
try:
validate_code(code_str)
@@ -130,7 +135,9 @@ def analyze_data_task(
expected_hash = hashlib.sha256(expected_bytes).hexdigest()
with contextlib.suppress(CompetitionTaskSubmission.DoesNotExist):
submission = CompetitionTaskSubmission.objects.get(id=submission_id)
submission = CompetitionTaskSubmission.objects.get(
id=submission_id
)
submission.result = {"correct": True}
return {
@@ -28,5 +28,3 @@ with open("file.txt") as f:
print(result)
self.assertTrue(result["success"])
self.assertTrue(result["match"])
+3 -2
View File
@@ -17,8 +17,9 @@ class User(BaseModel):
created_at = models.DateTimeField(auto_now=True)
achievements = models.ManyToManyField(Achievement, blank=True,
verbose_name="ачивки пользователя")
achievements = models.ManyToManyField(
Achievement, blank=True, verbose_name="ачивки пользователя"
)
@staticmethod
def make_password(password: str):
+23 -14
View File
@@ -478,29 +478,38 @@ TINYMCE_DEFAULT_CONFIG = {
{"start": "######", "format": "h6"},
{"start": "1. ", "cmd": "InsertOrderedList"},
{"start": "* ", "cmd": "InsertUnorderedList"},
{"start": "- ", "cmd": "InsertUnorderedList"}
]
{"start": "- ", "cmd": "InsertUnorderedList"},
],
}
# martor
MARTOR_THEME = 'bootstrap'
MARTOR_THEME = "bootstrap"
MARTOR_ENABLE_CONFIGS = {
'emoji': 'true', # to enable/disable emoji icons.
'imgur': 'true', # to enable/disable imgur/custom uploader.
'mention': 'false', # to enable/disable mention
'jquery': 'true', # to include/revoke jquery (require for admin default django)
'living': 'false', # to enable/disable live updates in preview
'spellcheck': 'false', # to enable/disable spellcheck in form textareas
'hljs': 'true', # to enable/disable hljs highlighting in preview
"emoji": "true", # to enable/disable emoji icons.
"imgur": "true", # to enable/disable imgur/custom uploader.
"mention": "false", # to enable/disable mention
"jquery": "true", # to include/revoke jquery (require for admin default django)
"living": "false", # to enable/disable live updates in preview
"spellcheck": "false", # to enable/disable spellcheck in form textareas
"hljs": "true", # to enable/disable hljs highlighting in preview
}
MARTOR_TOOLBAR_BUTTONS = [
'bold', 'italic', 'horizontal', 'heading', 'pre-code',
'blockquote', 'unordered-list', 'ordered-list',
'link', 'emoji',
'direct-mention', 'toggle-maximize', 'help'
"bold",
"italic",
"horizontal",
"heading",
"pre-code",
"blockquote",
"unordered-list",
"ordered-list",
"link",
"emoji",
"direct-mention",
"toggle-maximize",
"help",
]
# GUID
+1 -1
View File
@@ -15,7 +15,7 @@ urlpatterns = [
# tinymce
path("tinymce/", include("tinymce.urls")),
# martor
path('martor/', include('martor.urls')),
path("martor/", include("martor.urls")),
# Admin urls
path("admin/", admin.site.urls),
# API urls