删除无用代码并且新增流程图相关内容
This commit is contained in:
@@ -210,7 +210,6 @@ class LanguagesAPI(APIView):
|
|||||||
return self.success(
|
return self.success(
|
||||||
{
|
{
|
||||||
"languages": SysOptions.languages,
|
"languages": SysOptions.languages,
|
||||||
"spj_languages": SysOptions.spj_languages,
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
0
flowchart/__init__.py
Normal file
0
flowchart/__init__.py
Normal file
0
flowchart/admin.py
Normal file
0
flowchart/admin.py
Normal file
7
flowchart/apps.py
Normal file
7
flowchart/apps.py
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class FlowchartConfig(AppConfig):
|
||||||
|
default_auto_field = 'django.db.models.BigAutoField'
|
||||||
|
name = 'flowchart'
|
||||||
|
verbose_name = '流程图管理'
|
||||||
83
flowchart/consumers.py
Normal file
83
flowchart/consumers.py
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
"""
|
||||||
|
WebSocket consumers for flowchart evaluation updates
|
||||||
|
"""
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
from channels.generic.websocket import AsyncWebsocketConsumer
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class FlowchartConsumer(AsyncWebsocketConsumer):
|
||||||
|
"""
|
||||||
|
WebSocket consumer for real-time flowchart evaluation updates
|
||||||
|
当用户提交流程图后,通过 WebSocket 实时接收AI评分状态更新
|
||||||
|
"""
|
||||||
|
|
||||||
|
async def connect(self):
|
||||||
|
"""处理 WebSocket 连接"""
|
||||||
|
self.user = self.scope["user"]
|
||||||
|
|
||||||
|
# 只允许认证用户连接
|
||||||
|
if not self.user.is_authenticated:
|
||||||
|
await self.close()
|
||||||
|
return
|
||||||
|
|
||||||
|
# 使用用户 ID 作为组名,这样可以向特定用户推送消息
|
||||||
|
self.group_name = f"flowchart_user_{self.user.id}"
|
||||||
|
|
||||||
|
# 加入用户专属的组
|
||||||
|
await self.channel_layer.group_add(
|
||||||
|
self.group_name,
|
||||||
|
self.channel_name
|
||||||
|
)
|
||||||
|
|
||||||
|
await self.accept()
|
||||||
|
logger.info(f"Flowchart WebSocket connected: user_id={self.user.id}, channel={self.channel_name}")
|
||||||
|
|
||||||
|
async def disconnect(self, close_code):
|
||||||
|
"""处理 WebSocket 断开连接"""
|
||||||
|
if hasattr(self, 'group_name'):
|
||||||
|
await self.channel_layer.group_discard(
|
||||||
|
self.group_name,
|
||||||
|
self.channel_name
|
||||||
|
)
|
||||||
|
logger.info(f"Flowchart WebSocket disconnected: user_id={self.user.id}, close_code={close_code}")
|
||||||
|
|
||||||
|
async def receive(self, text_data):
|
||||||
|
"""
|
||||||
|
接收客户端消息
|
||||||
|
客户端可以发送心跳包或订阅特定流程图提交
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
data = json.loads(text_data)
|
||||||
|
message_type = data.get("type")
|
||||||
|
|
||||||
|
if message_type == "ping":
|
||||||
|
# 响应心跳包
|
||||||
|
await self.send(text_data=json.dumps({
|
||||||
|
"type": "pong",
|
||||||
|
"timestamp": data.get("timestamp")
|
||||||
|
}))
|
||||||
|
elif message_type == "subscribe":
|
||||||
|
# 订阅特定流程图提交的更新
|
||||||
|
submission_id = data.get("submission_id")
|
||||||
|
if submission_id:
|
||||||
|
logger.info(f"User {self.user.id} subscribed to flowchart submission {submission_id}")
|
||||||
|
# 可以在这里做额外的订阅逻辑
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
logger.error(f"Invalid JSON received from user {self.user.id}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error handling message from user {self.user.id}: {str(e)}")
|
||||||
|
|
||||||
|
async def flowchart_evaluation_update(self, event):
|
||||||
|
"""
|
||||||
|
接收来自 channel layer 的流程图评分更新消息并发送给客户端
|
||||||
|
这个方法名对应 push_flowchart_evaluation_update 中的 type 字段
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# 从 event 中提取数据并发送给客户端
|
||||||
|
await self.send(text_data=json.dumps(event["data"]))
|
||||||
|
logger.debug(f"Sent flowchart evaluation update to user {self.user.id}: {event['data']}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error sending flowchart evaluation update to user {self.user.id}: {str(e)}")
|
||||||
45
flowchart/migrations/0001_initial.py
Normal file
45
flowchart/migrations/0001_initial.py
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
# Generated by Django 5.2.3 on 2025-10-11 14:57
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
import utils.shortcuts
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('problem', '0004_problem_allow_flowchart_problem_flowchart_data_and_more'),
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='FlowchartSubmission',
|
||||||
|
fields=[
|
||||||
|
('id', models.TextField(db_index=True, default=utils.shortcuts.rand_str, primary_key=True, serialize=False)),
|
||||||
|
('mermaid_code', models.TextField()),
|
||||||
|
('flowchart_data', models.JSONField(default=dict)),
|
||||||
|
('status', models.IntegerField(default=0)),
|
||||||
|
('create_time', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('ai_score', models.FloatField(blank=True, null=True)),
|
||||||
|
('ai_grade', models.CharField(blank=True, max_length=10, null=True)),
|
||||||
|
('ai_feedback', models.TextField(blank=True, null=True)),
|
||||||
|
('ai_suggestions', models.TextField(blank=True, null=True)),
|
||||||
|
('ai_criteria_details', models.JSONField(default=dict)),
|
||||||
|
('ai_provider', models.CharField(default='deepseek', max_length=50)),
|
||||||
|
('ai_model', models.CharField(default='deepseek-chat', max_length=50)),
|
||||||
|
('processing_time', models.FloatField(blank=True, null=True)),
|
||||||
|
('evaluation_time', models.DateTimeField(blank=True, null=True)),
|
||||||
|
('problem', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='flowchart_submissions', to='problem.problem')),
|
||||||
|
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='flowchart_submissions', to=settings.AUTH_USER_MODEL)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'db_table': 'flowchart_submission',
|
||||||
|
'ordering': ['-create_time'],
|
||||||
|
'indexes': [models.Index(fields=['user', 'create_time'], name='flowchart_user_time_idx'), models.Index(fields=['problem', 'create_time'], name='flowchart_problem_time_idx'), models.Index(fields=['status'], name='flowchart_status_idx')],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
0
flowchart/migrations/__init__.py
Normal file
0
flowchart/migrations/__init__.py
Normal file
65
flowchart/models.py
Normal file
65
flowchart/models.py
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
from django.db import models
|
||||||
|
from django.contrib.auth import get_user_model
|
||||||
|
from utils.shortcuts import rand_str
|
||||||
|
from problem.models import Problem
|
||||||
|
|
||||||
|
User = get_user_model()
|
||||||
|
|
||||||
|
class FlowchartSubmissionStatus:
|
||||||
|
PENDING = 0 # 等待AI评分
|
||||||
|
PROCESSING = 1 # AI评分中
|
||||||
|
COMPLETED = 2 # 评分完成
|
||||||
|
FAILED = 3 # 评分失败
|
||||||
|
|
||||||
|
class FlowchartSubmission(models.Model):
|
||||||
|
"""流程图提交模型"""
|
||||||
|
id = models.TextField(default=rand_str, primary_key=True, db_index=True)
|
||||||
|
|
||||||
|
# 基础信息
|
||||||
|
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='flowchart_submissions')
|
||||||
|
problem = models.ForeignKey(Problem, on_delete=models.CASCADE, related_name='flowchart_submissions')
|
||||||
|
|
||||||
|
# 提交内容
|
||||||
|
mermaid_code = models.TextField() # Mermaid代码
|
||||||
|
flowchart_data = models.JSONField(default=dict) # 流程图元数据
|
||||||
|
|
||||||
|
# 状态信息
|
||||||
|
status = models.IntegerField(default=FlowchartSubmissionStatus.PENDING)
|
||||||
|
create_time = models.DateTimeField(auto_now_add=True)
|
||||||
|
|
||||||
|
# AI评分结果
|
||||||
|
ai_score = models.FloatField(null=True, blank=True) # AI评分 (0-100)
|
||||||
|
ai_grade = models.CharField(max_length=10, null=True, blank=True) # 等级 (S/A/B/C)
|
||||||
|
ai_feedback = models.TextField(null=True, blank=True) # AI反馈
|
||||||
|
ai_suggestions = models.TextField(null=True, blank=True) # AI建议
|
||||||
|
ai_criteria_details = models.JSONField(default=dict) # 详细评分标准
|
||||||
|
|
||||||
|
# 处理信息
|
||||||
|
ai_provider = models.CharField(max_length=50, default='deepseek')
|
||||||
|
ai_model = models.CharField(max_length=50, default='deepseek-chat')
|
||||||
|
processing_time = models.FloatField(null=True, blank=True) # AI处理耗时(秒)
|
||||||
|
evaluation_time = models.DateTimeField(null=True, blank=True) # 评分完成时间
|
||||||
|
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
db_table = 'flowchart_submission'
|
||||||
|
ordering = ['-create_time']
|
||||||
|
indexes = [
|
||||||
|
models.Index(fields=['user', 'create_time'], name='flowchart_user_time_idx'),
|
||||||
|
models.Index(fields=['problem', 'create_time'], name='flowchart_problem_time_idx'),
|
||||||
|
models.Index(fields=['status'], name='flowchart_status_idx'),
|
||||||
|
]
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"FlowchartSubmission {self.id}"
|
||||||
|
|
||||||
|
def check_user_permission(self, user, check_share=True):
|
||||||
|
"""检查用户权限"""
|
||||||
|
if (
|
||||||
|
self.user_id == user.id
|
||||||
|
or not user.is_regular_user()
|
||||||
|
or self.problem.created_by_id == user.id
|
||||||
|
):
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
60
flowchart/serializers.py
Normal file
60
flowchart/serializers.py
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
from rest_framework import serializers
|
||||||
|
from .models import FlowchartSubmission
|
||||||
|
|
||||||
|
|
||||||
|
class CreateFlowchartSubmissionSerializer(serializers.Serializer):
|
||||||
|
problem_id = serializers.IntegerField()
|
||||||
|
mermaid_code = serializers.CharField()
|
||||||
|
flowchart_data = serializers.JSONField(required=False, default=dict)
|
||||||
|
|
||||||
|
def validate_mermaid_code(self, value):
|
||||||
|
if not value.strip():
|
||||||
|
raise serializers.ValidationError("Mermaid代码不能为空")
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
class FlowchartSubmissionSerializer(serializers.ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = FlowchartSubmission
|
||||||
|
fields = [
|
||||||
|
"id",
|
||||||
|
"user",
|
||||||
|
"problem",
|
||||||
|
"mermaid_code",
|
||||||
|
"flowchart_data",
|
||||||
|
"status",
|
||||||
|
"create_time",
|
||||||
|
"ai_score",
|
||||||
|
"ai_grade",
|
||||||
|
"ai_feedback",
|
||||||
|
"ai_suggestions",
|
||||||
|
"ai_criteria_details",
|
||||||
|
"ai_provider",
|
||||||
|
"ai_model",
|
||||||
|
"processing_time",
|
||||||
|
"evaluation_time",
|
||||||
|
]
|
||||||
|
read_only_fields = ["id", "create_time", "evaluation_time"]
|
||||||
|
|
||||||
|
|
||||||
|
class FlowchartSubmissionListSerializer(serializers.ModelSerializer):
|
||||||
|
"""用于列表显示的简化序列化器"""
|
||||||
|
|
||||||
|
username = serializers.CharField(source="user.username")
|
||||||
|
problem_title = serializers.CharField(source="problem.title")
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = FlowchartSubmission
|
||||||
|
fields = [
|
||||||
|
"id",
|
||||||
|
"username",
|
||||||
|
"problem_title",
|
||||||
|
"status",
|
||||||
|
"create_time",
|
||||||
|
"ai_score",
|
||||||
|
"ai_grade",
|
||||||
|
"ai_provider",
|
||||||
|
"ai_model",
|
||||||
|
"processing_time",
|
||||||
|
"evaluation_time",
|
||||||
|
]
|
||||||
186
flowchart/tasks.py
Normal file
186
flowchart/tasks.py
Normal file
@@ -0,0 +1,186 @@
|
|||||||
|
import dramatiq
|
||||||
|
import json
|
||||||
|
import time
|
||||||
|
from openai import OpenAI
|
||||||
|
from django.db import transaction
|
||||||
|
from django.utils import timezone
|
||||||
|
from utils.shortcuts import get_env, DRAMATIQ_WORKER_ARGS
|
||||||
|
from .models import FlowchartSubmission, FlowchartSubmissionStatus
|
||||||
|
|
||||||
|
@dramatiq.actor(**DRAMATIQ_WORKER_ARGS(max_retries=3))
|
||||||
|
def evaluate_flowchart_task(submission_id):
|
||||||
|
"""异步AI评分任务"""
|
||||||
|
try:
|
||||||
|
submission = FlowchartSubmission.objects.get(id=submission_id)
|
||||||
|
|
||||||
|
# 更新状态为处理中
|
||||||
|
submission.status = FlowchartSubmissionStatus.PROCESSING
|
||||||
|
submission.save()
|
||||||
|
|
||||||
|
start_time = time.time()
|
||||||
|
|
||||||
|
# 使用固定评分标准
|
||||||
|
system_prompt = build_evaluation_prompt(submission.problem)
|
||||||
|
|
||||||
|
# 构建用户提示词,包含标准答案对比
|
||||||
|
user_prompt = f"""
|
||||||
|
请对以下Mermaid流程图进行评分:
|
||||||
|
|
||||||
|
学生提交的流程图:
|
||||||
|
```mermaid
|
||||||
|
{submission.mermaid_code}
|
||||||
|
```
|
||||||
|
|
||||||
|
标准答案参考:
|
||||||
|
```mermaid
|
||||||
|
{submission.problem.mermaid_code}
|
||||||
|
```
|
||||||
|
"""
|
||||||
|
# 如果有流程图提示,添加到提示词中
|
||||||
|
if submission.problem.flowchart_hint:
|
||||||
|
user_prompt += f"""
|
||||||
|
|
||||||
|
设计提示:{submission.problem.flowchart_hint}
|
||||||
|
"""
|
||||||
|
|
||||||
|
user_prompt += """
|
||||||
|
|
||||||
|
请按照评分标准进行详细评估,并给出0-100的分数。
|
||||||
|
"""
|
||||||
|
|
||||||
|
# 调用AI进行评分
|
||||||
|
api_key = get_env("AI_KEY")
|
||||||
|
if not api_key:
|
||||||
|
raise Exception("AI_KEY is not set")
|
||||||
|
|
||||||
|
client = OpenAI(api_key=api_key, base_url="https://api.deepseek.com")
|
||||||
|
|
||||||
|
response = client.chat.completions.create(
|
||||||
|
model="deepseek-chat",
|
||||||
|
messages=[
|
||||||
|
{"role": "system", "content": system_prompt},
|
||||||
|
{"role": "user", "content": user_prompt}
|
||||||
|
],
|
||||||
|
temperature=0.3,
|
||||||
|
)
|
||||||
|
|
||||||
|
ai_response = response.choices[0].message.content
|
||||||
|
score_data = parse_ai_evaluation_response(ai_response)
|
||||||
|
|
||||||
|
processing_time = time.time() - start_time
|
||||||
|
|
||||||
|
# 保存评分结果
|
||||||
|
with transaction.atomic():
|
||||||
|
submission.ai_score = score_data['score']
|
||||||
|
submission.ai_grade = score_data['grade']
|
||||||
|
submission.ai_feedback = score_data['feedback']
|
||||||
|
submission.ai_suggestions = score_data.get('suggestions', '')
|
||||||
|
submission.ai_criteria_details = score_data.get('criteria_details', {})
|
||||||
|
submission.ai_provider = 'deepseek'
|
||||||
|
submission.ai_model = 'deepseek-chat'
|
||||||
|
submission.processing_time = processing_time
|
||||||
|
submission.status = FlowchartSubmissionStatus.COMPLETED
|
||||||
|
submission.evaluation_time = timezone.now()
|
||||||
|
submission.save()
|
||||||
|
|
||||||
|
# 推送评分完成通知
|
||||||
|
from utils.websocket import push_flowchart_evaluation_update
|
||||||
|
push_flowchart_evaluation_update(
|
||||||
|
submission_id=str(submission.id),
|
||||||
|
user_id=submission.user_id,
|
||||||
|
data={
|
||||||
|
"type": "flowchart_evaluation_completed",
|
||||||
|
"submission_id": str(submission.id),
|
||||||
|
"score": score_data['score'],
|
||||||
|
"grade": score_data['grade'],
|
||||||
|
"feedback": score_data['feedback']
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
# 处理失败
|
||||||
|
submission.status = FlowchartSubmissionStatus.FAILED
|
||||||
|
submission.save()
|
||||||
|
|
||||||
|
# 推送错误通知
|
||||||
|
from utils.websocket import push_flowchart_evaluation_update
|
||||||
|
push_flowchart_evaluation_update(
|
||||||
|
submission_id=str(submission.id),
|
||||||
|
user_id=submission.user_id,
|
||||||
|
data={
|
||||||
|
"type": "flowchart_evaluation_failed",
|
||||||
|
"submission_id": str(submission.id),
|
||||||
|
"error": str(e)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
raise e
|
||||||
|
|
||||||
|
def build_evaluation_prompt(problem):
|
||||||
|
"""构建AI评分提示词 - 使用固定标准"""
|
||||||
|
|
||||||
|
# 使用固定的评分标准
|
||||||
|
criteria_text = """
|
||||||
|
- 逻辑正确性 (权重: 1.0, 最高分: 40): 检查流程图的逻辑是否正确,包括条件判断、循环结构等
|
||||||
|
- 完整性 (权重: 0.8, 最高分: 30): 检查流程图是否包含所有必要的步骤和分支
|
||||||
|
- 规范性 (权重: 0.6, 最高分: 20): 检查流程图符号使用是否规范,是否符合标准
|
||||||
|
- 清晰度 (权重: 0.4, 最高分: 10): 评估流程图的整体布局和可读性
|
||||||
|
"""
|
||||||
|
|
||||||
|
return f"""
|
||||||
|
你是一个专业的编程教学助手,负责评估学生提交的Mermaid流程图。
|
||||||
|
|
||||||
|
评分标准:
|
||||||
|
{criteria_text}
|
||||||
|
|
||||||
|
评分要求:
|
||||||
|
1. 仔细分析流程图的逻辑正确性、完整性和清晰度
|
||||||
|
2. 检查是否涵盖了题目的所有要求
|
||||||
|
3. 评估流程图的规范性和可读性
|
||||||
|
4. 给出0-100的分数
|
||||||
|
5. 提供详细的反馈和改进建议
|
||||||
|
|
||||||
|
评分等级:
|
||||||
|
- S级 (90-100分): 优秀,逻辑清晰,完全符合要求
|
||||||
|
- A级 (80-89分): 良好,基本符合要求,有少量改进空间
|
||||||
|
- B级 (70-79分): 及格,基本正确但存在一些问题
|
||||||
|
- C级 (0-69分): 需要改进,存在明显问题
|
||||||
|
|
||||||
|
请以JSON格式返回评分结果:
|
||||||
|
{{
|
||||||
|
"score": 85,
|
||||||
|
"grade": "A",
|
||||||
|
"feedback": "详细的反馈内容",
|
||||||
|
"suggestions": "改进建议",
|
||||||
|
"criteria_details": {{
|
||||||
|
"逻辑正确性": {{"score": 35, "max": 40, "comment": "逻辑基本正确"}},
|
||||||
|
"完整性": {{"score": 25, "max": 30, "comment": "缺少部分步骤"}},
|
||||||
|
"规范性": {{"score": 18, "max": 20, "comment": "符号使用规范"}},
|
||||||
|
"清晰度": {{"score": 8, "max": 10, "comment": "布局清晰"}}
|
||||||
|
}}
|
||||||
|
}}
|
||||||
|
"""
|
||||||
|
|
||||||
|
def parse_ai_evaluation_response(ai_response):
|
||||||
|
"""解析AI评分响应"""
|
||||||
|
try:
|
||||||
|
import re
|
||||||
|
json_match = re.search(r'\{.*\}', ai_response, re.DOTALL)
|
||||||
|
if json_match:
|
||||||
|
data = json.loads(json_match.group())
|
||||||
|
else:
|
||||||
|
data = {
|
||||||
|
"score": 60,
|
||||||
|
"grade": "C",
|
||||||
|
"feedback": "AI评分解析失败,请重新提交",
|
||||||
|
"suggestions": "",
|
||||||
|
"criteria_details": {}
|
||||||
|
}
|
||||||
|
return data
|
||||||
|
except Exception:
|
||||||
|
return {
|
||||||
|
"score": 60,
|
||||||
|
"grade": "C",
|
||||||
|
"feedback": "AI评分解析失败,请重新提交",
|
||||||
|
"suggestions": "",
|
||||||
|
"criteria_details": {}
|
||||||
|
}
|
||||||
1
flowchart/urls/__init__.py
Normal file
1
flowchart/urls/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
# URLs package
|
||||||
12
flowchart/urls/oj.py
Normal file
12
flowchart/urls/oj.py
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
from django.urls import path
|
||||||
|
from ..views.oj import (
|
||||||
|
FlowchartSubmissionAPI,
|
||||||
|
FlowchartSubmissionListAPI,
|
||||||
|
FlowchartSubmissionRetryAPI
|
||||||
|
)
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
path('flowchart/submission', FlowchartSubmissionAPI.as_view()),
|
||||||
|
path('flowchart/submissions', FlowchartSubmissionListAPI.as_view()),
|
||||||
|
path('flowchart/submission/retry', FlowchartSubmissionRetryAPI.as_view()),
|
||||||
|
]
|
||||||
3
flowchart/views.py
Normal file
3
flowchart/views.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
from django.shortcuts import render
|
||||||
|
|
||||||
|
# Create your views here.
|
||||||
1
flowchart/views/__init__.py
Normal file
1
flowchart/views/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
# Views package
|
||||||
138
flowchart/views/oj.py
Normal file
138
flowchart/views/oj.py
Normal file
@@ -0,0 +1,138 @@
|
|||||||
|
from utils.api import APIView
|
||||||
|
from account.decorators import login_required
|
||||||
|
from flowchart.models import FlowchartSubmission, FlowchartSubmissionStatus
|
||||||
|
from flowchart.serializers import (
|
||||||
|
CreateFlowchartSubmissionSerializer,
|
||||||
|
FlowchartSubmissionSerializer,
|
||||||
|
FlowchartSubmissionListSerializer
|
||||||
|
)
|
||||||
|
from flowchart.tasks import evaluate_flowchart_task
|
||||||
|
|
||||||
|
class FlowchartSubmissionAPI(APIView):
|
||||||
|
@login_required
|
||||||
|
def post(self, request):
|
||||||
|
"""创建流程图提交"""
|
||||||
|
serializer = CreateFlowchartSubmissionSerializer(data=request.data)
|
||||||
|
if not serializer.is_valid():
|
||||||
|
return self.error(serializer.errors)
|
||||||
|
|
||||||
|
data = serializer.validated_data
|
||||||
|
|
||||||
|
# 验证题目存在
|
||||||
|
try:
|
||||||
|
from problem.models import Problem
|
||||||
|
problem = Problem.objects.get(_id=data['problem_id'])
|
||||||
|
except Problem.DoesNotExist:
|
||||||
|
return self.error("Problem doesn't exist")
|
||||||
|
|
||||||
|
# 验证题目是否允许流程图提交
|
||||||
|
if not problem.allow_flowchart:
|
||||||
|
return self.error("This problem does not allow flowchart submission")
|
||||||
|
|
||||||
|
# 创建提交记录
|
||||||
|
submission = FlowchartSubmission.objects.create(
|
||||||
|
user=request.user,
|
||||||
|
problem=problem,
|
||||||
|
mermaid_code=data['mermaid_code'],
|
||||||
|
flowchart_data=data.get('flowchart_data', {})
|
||||||
|
)
|
||||||
|
|
||||||
|
# 启动AI评分任务
|
||||||
|
evaluate_flowchart_task.send(submission.id)
|
||||||
|
|
||||||
|
return self.success({
|
||||||
|
'submission_id': submission.id,
|
||||||
|
'status': 'pending'
|
||||||
|
})
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def get(self, request):
|
||||||
|
"""获取流程图提交详情"""
|
||||||
|
submission_id = request.GET.get('id')
|
||||||
|
if not submission_id:
|
||||||
|
return self.error("submission_id is required")
|
||||||
|
|
||||||
|
try:
|
||||||
|
submission = FlowchartSubmission.objects.get(id=submission_id)
|
||||||
|
except FlowchartSubmission.DoesNotExist:
|
||||||
|
return self.error("Submission doesn't exist")
|
||||||
|
|
||||||
|
if not submission.check_user_permission(request.user):
|
||||||
|
return self.error("No permission for this submission")
|
||||||
|
|
||||||
|
serializer = FlowchartSubmissionSerializer(submission)
|
||||||
|
return self.success(serializer.data)
|
||||||
|
|
||||||
|
class FlowchartSubmissionListAPI(APIView):
|
||||||
|
@login_required
|
||||||
|
def get(self, request):
|
||||||
|
"""获取流程图提交列表"""
|
||||||
|
user_id = request.GET.get('user_id')
|
||||||
|
problem_id = request.GET.get('problem_id')
|
||||||
|
offset = int(request.GET.get('offset', 0))
|
||||||
|
limit = int(request.GET.get('limit', 20))
|
||||||
|
|
||||||
|
queryset = FlowchartSubmission.objects.select_related('user', 'problem')
|
||||||
|
|
||||||
|
# 权限过滤
|
||||||
|
if not request.user.is_admin_role():
|
||||||
|
queryset = queryset.filter(user=request.user)
|
||||||
|
|
||||||
|
# 其他过滤条件
|
||||||
|
if user_id:
|
||||||
|
queryset = queryset.filter(user_id=user_id)
|
||||||
|
if problem_id:
|
||||||
|
queryset = queryset.filter(problem_id=problem_id)
|
||||||
|
|
||||||
|
total = queryset.count()
|
||||||
|
submissions = queryset[offset:offset + limit]
|
||||||
|
|
||||||
|
serializer = FlowchartSubmissionListSerializer(submissions, many=True)
|
||||||
|
|
||||||
|
return self.success({
|
||||||
|
'results': serializer.data,
|
||||||
|
'total': total
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
class FlowchartSubmissionRetryAPI(APIView):
|
||||||
|
@login_required
|
||||||
|
def post(self, request):
|
||||||
|
"""重新触发AI评分"""
|
||||||
|
submission_id = request.data.get('submission_id')
|
||||||
|
if not submission_id:
|
||||||
|
return self.error("submission_id is required")
|
||||||
|
|
||||||
|
try:
|
||||||
|
submission = FlowchartSubmission.objects.get(id=submission_id)
|
||||||
|
except FlowchartSubmission.DoesNotExist:
|
||||||
|
return self.error("Submission doesn't exist")
|
||||||
|
|
||||||
|
# 检查权限
|
||||||
|
if not submission.check_user_permission(request.user):
|
||||||
|
return self.error("No permission for this submission")
|
||||||
|
|
||||||
|
# 检查是否可以重新评分
|
||||||
|
if submission.status not in [FlowchartSubmissionStatus.FAILED, FlowchartSubmissionStatus.COMPLETED]:
|
||||||
|
return self.error("Submission is not in a state that allows retry")
|
||||||
|
|
||||||
|
# 重置状态并重新启动AI评分
|
||||||
|
submission.status = FlowchartSubmissionStatus.PENDING
|
||||||
|
submission.ai_score = None
|
||||||
|
submission.ai_grade = None
|
||||||
|
submission.ai_feedback = None
|
||||||
|
submission.ai_suggestions = None
|
||||||
|
submission.ai_criteria_details = {}
|
||||||
|
submission.processing_time = None
|
||||||
|
submission.evaluation_time = None
|
||||||
|
submission.save()
|
||||||
|
|
||||||
|
# 重新启动AI评分任务
|
||||||
|
evaluate_flowchart_task.send(submission.id)
|
||||||
|
|
||||||
|
return self.success({
|
||||||
|
'submission_id': submission.id,
|
||||||
|
'status': 'pending',
|
||||||
|
'message': 'AI evaluation restarted'
|
||||||
|
})
|
||||||
|
|
||||||
@@ -67,26 +67,6 @@ class DispatcherBase(object):
|
|||||||
logger.exception(e)
|
logger.exception(e)
|
||||||
|
|
||||||
|
|
||||||
class SPJCompiler(DispatcherBase):
|
|
||||||
def __init__(self, spj_code, spj_version, spj_language):
|
|
||||||
super().__init__()
|
|
||||||
spj_compile_config = list(filter(lambda config: spj_language == config["name"], SysOptions.spj_languages))[0]["spj"][
|
|
||||||
"compile"]
|
|
||||||
self.data = {
|
|
||||||
"src": spj_code,
|
|
||||||
"spj_version": spj_version,
|
|
||||||
"spj_compile_config": spj_compile_config
|
|
||||||
}
|
|
||||||
|
|
||||||
def compile_spj(self):
|
|
||||||
with ChooseJudgeServer() as server:
|
|
||||||
if not server:
|
|
||||||
return "No available judge_server"
|
|
||||||
result = self._request(urljoin(server.service_url, "compile_spj"), data=self.data)
|
|
||||||
if not result:
|
|
||||||
return "Failed to call judge server"
|
|
||||||
if result["err"]:
|
|
||||||
return result["data"]
|
|
||||||
|
|
||||||
|
|
||||||
class JudgeDispatcher(DispatcherBase):
|
class JudgeDispatcher(DispatcherBase):
|
||||||
@@ -126,12 +106,6 @@ class JudgeDispatcher(DispatcherBase):
|
|||||||
def judge(self):
|
def judge(self):
|
||||||
language = self.submission.language
|
language = self.submission.language
|
||||||
sub_config = list(filter(lambda item: language == item["name"], SysOptions.languages))[0]
|
sub_config = list(filter(lambda item: language == item["name"], SysOptions.languages))[0]
|
||||||
spj_config = {}
|
|
||||||
if self.problem.spj_code:
|
|
||||||
for lang in SysOptions.spj_languages:
|
|
||||||
if lang["name"] == self.problem.spj_language:
|
|
||||||
spj_config = lang["spj"]
|
|
||||||
break
|
|
||||||
|
|
||||||
if language in self.problem.template:
|
if language in self.problem.template:
|
||||||
template = parse_problem_template(self.problem.template[language])
|
template = parse_problem_template(self.problem.template[language])
|
||||||
@@ -146,10 +120,6 @@ class JudgeDispatcher(DispatcherBase):
|
|||||||
"max_memory": 1024 * 1024 * self.problem.memory_limit,
|
"max_memory": 1024 * 1024 * self.problem.memory_limit,
|
||||||
"test_case_id": self.problem.test_case_id,
|
"test_case_id": self.problem.test_case_id,
|
||||||
"output": False,
|
"output": False,
|
||||||
"spj_version": self.problem.spj_version,
|
|
||||||
"spj_config": spj_config.get("config"),
|
|
||||||
"spj_compile_config": spj_config.get("compile"),
|
|
||||||
"spj_src": self.problem.spj_code,
|
|
||||||
"io_mode": self.problem.io_mode
|
"io_mode": self.problem.io_mode
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -35,20 +35,6 @@ int main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_c_lang_spj_compile = {
|
|
||||||
"src_name": "spj-{spj_version}.c",
|
|
||||||
"exe_name": "spj-{spj_version}",
|
|
||||||
"max_cpu_time": 3000,
|
|
||||||
"max_real_time": 10000,
|
|
||||||
"max_memory": 1024 * 1024 * 1024,
|
|
||||||
"compile_command": "/usr/bin/gcc -DONLINE_JUDGE -O2 -w -fmax-errors=3 -std=c17 {src_path} -lm -o {exe_path}"
|
|
||||||
}
|
|
||||||
|
|
||||||
_c_lang_spj_config = {
|
|
||||||
"exe_name": "spj-{spj_version}",
|
|
||||||
"command": "{exe_path} {in_file_path} {user_out_file_path}",
|
|
||||||
"seccomp_rule": "c_cpp"
|
|
||||||
}
|
|
||||||
|
|
||||||
_cpp_lang_config = {
|
_cpp_lang_config = {
|
||||||
"template": """//PREPEND BEGIN
|
"template": """//PREPEND BEGIN
|
||||||
@@ -82,20 +68,6 @@ int main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_cpp_lang_spj_compile = {
|
|
||||||
"src_name": "spj-{spj_version}.cpp",
|
|
||||||
"exe_name": "spj-{spj_version}",
|
|
||||||
"max_cpu_time": 10000,
|
|
||||||
"max_real_time": 20000,
|
|
||||||
"max_memory": 1024 * 1024 * 1024,
|
|
||||||
"compile_command": "/usr/bin/g++ -DONLINE_JUDGE -O2 -w -fmax-errors=3 -std=c++20 {src_path} -lm -o {exe_path}"
|
|
||||||
}
|
|
||||||
|
|
||||||
_cpp_lang_spj_config = {
|
|
||||||
"exe_name": "spj-{spj_version}",
|
|
||||||
"command": "{exe_path} {in_file_path} {user_out_file_path}",
|
|
||||||
"seccomp_rule": "c_cpp"
|
|
||||||
}
|
|
||||||
|
|
||||||
_java_lang_config = {
|
_java_lang_config = {
|
||||||
"template": """//PREPEND BEGIN
|
"template": """//PREPEND BEGIN
|
||||||
@@ -224,10 +196,8 @@ console.log(add(1, 2))
|
|||||||
}
|
}
|
||||||
|
|
||||||
languages = [
|
languages = [
|
||||||
{"config": _c_lang_config, "name": "C", "description": "GCC 13", "content_type": "text/x-csrc",
|
{"config": _c_lang_config, "name": "C", "description": "GCC 13", "content_type": "text/x-csrc"},
|
||||||
"spj": {"compile": _c_lang_spj_compile, "config": _c_lang_spj_config}},
|
{"config": _cpp_lang_config, "name": "C++", "description": "GCC 13", "content_type": "text/x-c++src"},
|
||||||
{"config": _cpp_lang_config, "name": "C++", "description": "GCC 13", "content_type": "text/x-c++src",
|
|
||||||
"spj": {"compile": _cpp_lang_spj_compile, "config": _cpp_lang_spj_config}},
|
|
||||||
{"config": _java_lang_config, "name": "Java", "description": "Temurin 21", "content_type": "text/x-java"},
|
{"config": _java_lang_config, "name": "Java", "description": "Temurin 21", "content_type": "text/x-java"},
|
||||||
{"config": _py3_lang_config, "name": "Python3", "description": "Python 3.12", "content_type": "text/x-python"},
|
{"config": _py3_lang_config, "name": "Python3", "description": "Python 3.12", "content_type": "text/x-python"},
|
||||||
{"config": _go_lang_config, "name": "Golang", "description": "Golang 1.22", "content_type": "text/x-go"},
|
{"config": _go_lang_config, "name": "Golang", "description": "Golang 1.22", "content_type": "text/x-go"},
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|||||||
DATABASES = {
|
DATABASES = {
|
||||||
"default": {
|
"default": {
|
||||||
"ENGINE": "django.db.backends.postgresql",
|
"ENGINE": "django.db.backends.postgresql",
|
||||||
"HOST": "10.13.114.114",
|
"HOST": "150.158.29.156",
|
||||||
"PORT": "5433",
|
"PORT": "5432",
|
||||||
"NAME": "onlinejudge",
|
"NAME": "onlinejudge",
|
||||||
"USER": "onlinejudge",
|
"USER": "onlinejudge",
|
||||||
"PASSWORD": "onlinejudge",
|
"PASSWORD": "onlinejudge",
|
||||||
@@ -15,7 +15,7 @@ DATABASES = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
REDIS_CONF = {
|
REDIS_CONF = {
|
||||||
"host": "10.13.114.114",
|
"host": "150.158.29.156",
|
||||||
"port": 6379,
|
"port": 6379,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,9 +5,11 @@ WebSocket URL Configuration for oj project.
|
|||||||
from django.urls import path
|
from django.urls import path
|
||||||
from submission.consumers import SubmissionConsumer
|
from submission.consumers import SubmissionConsumer
|
||||||
from conf.consumers import ConfigConsumer
|
from conf.consumers import ConfigConsumer
|
||||||
|
from flowchart.consumers import FlowchartConsumer
|
||||||
|
|
||||||
websocket_urlpatterns = [
|
websocket_urlpatterns = [
|
||||||
path("ws/submission/", SubmissionConsumer.as_asgi()),
|
path("ws/submission/", SubmissionConsumer.as_asgi()),
|
||||||
path("ws/config/", ConfigConsumer.as_asgi()),
|
path("ws/config/", ConfigConsumer.as_asgi()),
|
||||||
|
path("ws/flowchart/", FlowchartConsumer.as_asgi()),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -58,6 +58,7 @@ LOCAL_APPS = [
|
|||||||
"comment",
|
"comment",
|
||||||
"tutorial",
|
"tutorial",
|
||||||
"ai",
|
"ai",
|
||||||
|
"flowchart",
|
||||||
]
|
]
|
||||||
|
|
||||||
INSTALLED_APPS = VENDOR_APPS + LOCAL_APPS
|
INSTALLED_APPS = VENDOR_APPS + LOCAL_APPS
|
||||||
|
|||||||
@@ -20,4 +20,5 @@ urlpatterns = [
|
|||||||
path("api/", include("tutorial.urls.tutorial")),
|
path("api/", include("tutorial.urls.tutorial")),
|
||||||
path("api/admin/", include("tutorial.urls.admin")),
|
path("api/admin/", include("tutorial.urls.admin")),
|
||||||
path("api/", include("ai.urls.oj")),
|
path("api/", include("ai.urls.oj")),
|
||||||
|
path("api/", include("flowchart.urls.oj")),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -273,18 +273,10 @@ class _SysOptionsMeta(type):
|
|||||||
def languages(cls, value):
|
def languages(cls, value):
|
||||||
cls._set_option(OptionKeys.languages, value)
|
cls._set_option(OptionKeys.languages, value)
|
||||||
|
|
||||||
@my_property(ttl=DEFAULT_SHORT_TTL)
|
|
||||||
def spj_languages(cls):
|
|
||||||
return [item for item in cls.languages if "spj" in item]
|
|
||||||
|
|
||||||
@my_property(ttl=DEFAULT_SHORT_TTL)
|
@my_property(ttl=DEFAULT_SHORT_TTL)
|
||||||
def language_names(cls):
|
def language_names(cls):
|
||||||
return [item["name"] for item in cls.languages]
|
return [item["name"] for item in cls.languages]
|
||||||
|
|
||||||
@my_property(ttl=DEFAULT_SHORT_TTL)
|
|
||||||
def spj_language_names(cls):
|
|
||||||
return [item["name"] for item in cls.languages if "spj" in item]
|
|
||||||
|
|
||||||
@my_property(ttl=DEFAULT_SHORT_TTL)
|
@my_property(ttl=DEFAULT_SHORT_TTL)
|
||||||
def enable_maxkb(cls):
|
def enable_maxkb(cls):
|
||||||
return cls._get_option(OptionKeys.enable_maxkb)
|
return cls._get_option(OptionKeys.enable_maxkb)
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
# Generated by Django 5.2.3 on 2025-10-11 14:57
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('problem', '0003_problem_answers'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='problem',
|
||||||
|
name='allow_flowchart',
|
||||||
|
field=models.BooleanField(default=False),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='problem',
|
||||||
|
name='flowchart_data',
|
||||||
|
field=models.JSONField(default=dict),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='problem',
|
||||||
|
name='flowchart_hint',
|
||||||
|
field=models.TextField(blank=True, null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='problem',
|
||||||
|
name='mermaid_code',
|
||||||
|
field=models.TextField(blank=True, null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='problem',
|
||||||
|
name='show_flowchart',
|
||||||
|
field=models.BooleanField(default=False),
|
||||||
|
),
|
||||||
|
]
|
||||||
33
problem/migrations/0005_remove_spj_fields.py
Normal file
33
problem/migrations/0005_remove_spj_fields.py
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
# Generated by Django 5.2.3 on 2025-10-11 15:22
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('problem', '0004_problem_allow_flowchart_problem_flowchart_data_and_more'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='problem',
|
||||||
|
name='spj',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='problem',
|
||||||
|
name='spj_code',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='problem',
|
||||||
|
name='spj_compile_ok',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='problem',
|
||||||
|
name='spj_language',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='problem',
|
||||||
|
name='spj_version',
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
from django.conf import settings
|
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
|
||||||
from account.models import User
|
from account.models import User
|
||||||
@@ -67,12 +66,6 @@ class Problem(models.Model):
|
|||||||
memory_limit = models.IntegerField()
|
memory_limit = models.IntegerField()
|
||||||
# io mode
|
# io mode
|
||||||
io_mode = models.JSONField(default=_default_io_mode)
|
io_mode = models.JSONField(default=_default_io_mode)
|
||||||
# special judge related
|
|
||||||
spj = models.BooleanField(default=False)
|
|
||||||
spj_language = models.TextField(null=True)
|
|
||||||
spj_code = models.TextField(null=True)
|
|
||||||
spj_version = models.TextField(null=True)
|
|
||||||
spj_compile_ok = models.BooleanField(default=False)
|
|
||||||
rule_type = models.TextField()
|
rule_type = models.TextField()
|
||||||
visible = models.BooleanField(default=True)
|
visible = models.BooleanField(default=True)
|
||||||
difficulty = models.TextField()
|
difficulty = models.TextField()
|
||||||
@@ -89,6 +82,13 @@ class Problem(models.Model):
|
|||||||
statistic_info = models.JSONField(default=dict)
|
statistic_info = models.JSONField(default=dict)
|
||||||
share_submission = models.BooleanField(default=False)
|
share_submission = models.BooleanField(default=False)
|
||||||
|
|
||||||
|
# 流程图相关字段
|
||||||
|
allow_flowchart = models.BooleanField(default=False) # 是否允许/需要提交流程图
|
||||||
|
mermaid_code = models.TextField(null=True, blank=True) # 流程图答案(Mermaid代码)
|
||||||
|
flowchart_data = models.JSONField(default=dict) # 流程图答案元数据(JSON格式)
|
||||||
|
flowchart_hint = models.TextField(null=True, blank=True) # 流程图提示信息
|
||||||
|
show_flowchart = models.BooleanField(default=False) # 是否显示流程图答案数据,如果True,这样就不需要提交流程图了,说明就是给学生看的
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
db_table = "problem"
|
db_table = "problem"
|
||||||
unique_together = (("_id", "contest"),)
|
unique_together = (("_id", "contest"),)
|
||||||
|
|||||||
@@ -2,12 +2,10 @@ import re
|
|||||||
|
|
||||||
from django import forms
|
from django import forms
|
||||||
|
|
||||||
from options.options import SysOptions
|
|
||||||
from utils.api import UsernameSerializer, serializers
|
from utils.api import UsernameSerializer, serializers
|
||||||
from utils.constants import Difficulty
|
from utils.constants import Difficulty
|
||||||
from utils.serializers import (
|
from utils.serializers import (
|
||||||
LanguageNameMultiChoiceField,
|
LanguageNameMultiChoiceField,
|
||||||
SPJLanguageNameChoiceField,
|
|
||||||
LanguageNameChoiceField,
|
LanguageNameChoiceField,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -16,7 +14,6 @@ from .utils import parse_problem_template
|
|||||||
|
|
||||||
|
|
||||||
class TestCaseUploadForm(forms.Form):
|
class TestCaseUploadForm(forms.Form):
|
||||||
spj = forms.CharField(max_length=12)
|
|
||||||
file = forms.FileField()
|
file = forms.FileField()
|
||||||
|
|
||||||
|
|
||||||
@@ -73,10 +70,6 @@ class CreateOrEditProblemSerializer(serializers.Serializer):
|
|||||||
choices=[ProblemRuleType.ACM, ProblemRuleType.OI]
|
choices=[ProblemRuleType.ACM, ProblemRuleType.OI]
|
||||||
)
|
)
|
||||||
io_mode = ProblemIOModeSerializer()
|
io_mode = ProblemIOModeSerializer()
|
||||||
spj = serializers.BooleanField()
|
|
||||||
spj_language = SPJLanguageNameChoiceField(allow_blank=True, allow_null=True)
|
|
||||||
spj_code = serializers.CharField(allow_blank=True, allow_null=True)
|
|
||||||
spj_compile_ok = serializers.BooleanField(default=False)
|
|
||||||
visible = serializers.BooleanField()
|
visible = serializers.BooleanField()
|
||||||
difficulty = serializers.ChoiceField(choices=Difficulty.choices())
|
difficulty = serializers.ChoiceField(choices=Difficulty.choices())
|
||||||
tags = serializers.ListField(
|
tags = serializers.ListField(
|
||||||
@@ -92,6 +85,17 @@ class CreateOrEditProblemSerializer(serializers.Serializer):
|
|||||||
)
|
)
|
||||||
share_submission = serializers.BooleanField()
|
share_submission = serializers.BooleanField()
|
||||||
|
|
||||||
|
# 流程图相关字段
|
||||||
|
allow_flowchart = serializers.BooleanField(required=False, default=False)
|
||||||
|
show_flowchart = serializers.BooleanField(required=False, default=False)
|
||||||
|
mermaid_code = serializers.CharField(
|
||||||
|
allow_blank=True, allow_null=True, required=False
|
||||||
|
)
|
||||||
|
|
||||||
|
flowchart_hint = serializers.CharField(
|
||||||
|
allow_blank=True, allow_null=True, required=False
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class CreateProblemSerializer(CreateOrEditProblemSerializer):
|
class CreateProblemSerializer(CreateOrEditProblemSerializer):
|
||||||
pass
|
pass
|
||||||
@@ -116,11 +120,6 @@ class TagSerializer(serializers.ModelSerializer):
|
|||||||
fields = "__all__"
|
fields = "__all__"
|
||||||
|
|
||||||
|
|
||||||
class CompileSPJSerializer(serializers.Serializer):
|
|
||||||
spj_language = SPJLanguageNameChoiceField()
|
|
||||||
spj_code = serializers.CharField()
|
|
||||||
|
|
||||||
|
|
||||||
class BaseProblemSerializer(serializers.ModelSerializer):
|
class BaseProblemSerializer(serializers.ModelSerializer):
|
||||||
tags = serializers.SlugRelatedField(many=True, slug_field="name", read_only=True)
|
tags = serializers.SlugRelatedField(many=True, slug_field="name", read_only=True)
|
||||||
created_by = UsernameSerializer()
|
created_by = UsernameSerializer()
|
||||||
@@ -154,9 +153,6 @@ class ProblemSerializer(BaseProblemSerializer):
|
|||||||
"test_case_id",
|
"test_case_id",
|
||||||
"visible",
|
"visible",
|
||||||
"is_public",
|
"is_public",
|
||||||
"spj_code",
|
|
||||||
"spj_version",
|
|
||||||
"spj_compile_ok",
|
|
||||||
"answers",
|
"answers",
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -188,9 +184,6 @@ class ProblemSafeSerializer(BaseProblemSerializer):
|
|||||||
"test_case_id",
|
"test_case_id",
|
||||||
"visible",
|
"visible",
|
||||||
"is_public",
|
"is_public",
|
||||||
"spj_code",
|
|
||||||
"spj_version",
|
|
||||||
"spj_compile_ok",
|
|
||||||
"difficulty",
|
"difficulty",
|
||||||
"submission_number",
|
"submission_number",
|
||||||
"accepted_number",
|
"accepted_number",
|
||||||
@@ -204,101 +197,12 @@ class ContestProblemMakePublicSerializer(serializers.Serializer):
|
|||||||
display_id = serializers.CharField(max_length=32)
|
display_id = serializers.CharField(max_length=32)
|
||||||
|
|
||||||
|
|
||||||
class ExportProblemSerializer(serializers.ModelSerializer):
|
|
||||||
display_id = serializers.SerializerMethodField()
|
|
||||||
description = serializers.SerializerMethodField()
|
|
||||||
input_description = serializers.SerializerMethodField()
|
|
||||||
output_description = serializers.SerializerMethodField()
|
|
||||||
test_case_score = serializers.SerializerMethodField()
|
|
||||||
hint = serializers.SerializerMethodField()
|
|
||||||
spj = serializers.SerializerMethodField()
|
|
||||||
template = serializers.SerializerMethodField()
|
|
||||||
source = serializers.SerializerMethodField()
|
|
||||||
tags = serializers.SlugRelatedField(many=True, slug_field="name", read_only=True)
|
|
||||||
|
|
||||||
def get_display_id(self, obj):
|
|
||||||
return obj._id
|
|
||||||
|
|
||||||
def _html_format_value(self, value):
|
|
||||||
return {"format": "html", "value": value}
|
|
||||||
|
|
||||||
def get_description(self, obj):
|
|
||||||
return self._html_format_value(obj.description)
|
|
||||||
|
|
||||||
def get_input_description(self, obj):
|
|
||||||
return self._html_format_value(obj.input_description)
|
|
||||||
|
|
||||||
def get_output_description(self, obj):
|
|
||||||
return self._html_format_value(obj.output_description)
|
|
||||||
|
|
||||||
def get_hint(self, obj):
|
|
||||||
return self._html_format_value(obj.hint)
|
|
||||||
|
|
||||||
def get_test_case_score(self, obj):
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
"score": item["score"] if obj.rule_type == ProblemRuleType.OI else 100,
|
|
||||||
"input_name": item["input_name"],
|
|
||||||
"output_name": item["output_name"],
|
|
||||||
}
|
|
||||||
for item in obj.test_case_score
|
|
||||||
]
|
|
||||||
|
|
||||||
def get_spj(self, obj):
|
|
||||||
return {"code": obj.spj_code, "language": obj.spj_language} if obj.spj else None
|
|
||||||
|
|
||||||
def get_template(self, obj):
|
|
||||||
ret = {}
|
|
||||||
for k, v in obj.template.items():
|
|
||||||
ret[k] = parse_problem_template(v)
|
|
||||||
return ret
|
|
||||||
|
|
||||||
def get_source(self, obj):
|
|
||||||
return obj.source or f"{SysOptions.website_name} {SysOptions.website_base_url}"
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = Problem
|
|
||||||
fields = (
|
|
||||||
"display_id",
|
|
||||||
"title",
|
|
||||||
"description",
|
|
||||||
"tags",
|
|
||||||
"input_description",
|
|
||||||
"output_description",
|
|
||||||
"test_case_score",
|
|
||||||
"hint",
|
|
||||||
"time_limit",
|
|
||||||
"memory_limit",
|
|
||||||
"samples",
|
|
||||||
"template",
|
|
||||||
"spj",
|
|
||||||
"rule_type",
|
|
||||||
"source",
|
|
||||||
"template",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class AddContestProblemSerializer(serializers.Serializer):
|
class AddContestProblemSerializer(serializers.Serializer):
|
||||||
contest_id = serializers.IntegerField()
|
contest_id = serializers.IntegerField()
|
||||||
problem_id = serializers.IntegerField()
|
problem_id = serializers.IntegerField()
|
||||||
display_id = serializers.CharField()
|
display_id = serializers.CharField()
|
||||||
|
|
||||||
|
|
||||||
class ExportProblemRequestSerializer(serializers.Serializer):
|
|
||||||
problem_id = serializers.ListField(
|
|
||||||
child=serializers.IntegerField(), allow_empty=False
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class UploadProblemForm(forms.Form):
|
|
||||||
file = forms.FileField()
|
|
||||||
|
|
||||||
|
|
||||||
class FormatValueSerializer(serializers.Serializer):
|
|
||||||
format = serializers.ChoiceField(choices=["html", "markdown"])
|
|
||||||
value = serializers.CharField(allow_blank=True)
|
|
||||||
|
|
||||||
|
|
||||||
class TestCaseScoreSerializer(serializers.Serializer):
|
class TestCaseScoreSerializer(serializers.Serializer):
|
||||||
score = serializers.IntegerField(min_value=1)
|
score = serializers.IntegerField(min_value=1)
|
||||||
input_name = serializers.CharField(max_length=32)
|
input_name = serializers.CharField(max_length=32)
|
||||||
@@ -311,58 +215,6 @@ class TemplateSerializer(serializers.Serializer):
|
|||||||
append = serializers.CharField()
|
append = serializers.CharField()
|
||||||
|
|
||||||
|
|
||||||
class SPJSerializer(serializers.Serializer):
|
|
||||||
code = serializers.CharField()
|
|
||||||
language = SPJLanguageNameChoiceField()
|
|
||||||
|
|
||||||
|
|
||||||
class AnswerSerializer(serializers.Serializer):
|
class AnswerSerializer(serializers.Serializer):
|
||||||
code = serializers.CharField()
|
code = serializers.CharField()
|
||||||
language = LanguageNameChoiceField()
|
language = LanguageNameChoiceField()
|
||||||
|
|
||||||
|
|
||||||
class ImportProblemSerializer(serializers.Serializer):
|
|
||||||
display_id = serializers.CharField(max_length=128)
|
|
||||||
title = serializers.CharField(max_length=128)
|
|
||||||
description = FormatValueSerializer()
|
|
||||||
input_description = FormatValueSerializer()
|
|
||||||
output_description = FormatValueSerializer()
|
|
||||||
hint = FormatValueSerializer()
|
|
||||||
test_case_score = serializers.ListField(
|
|
||||||
child=TestCaseScoreSerializer(), allow_null=True
|
|
||||||
)
|
|
||||||
time_limit = serializers.IntegerField(min_value=1, max_value=60000)
|
|
||||||
memory_limit = serializers.IntegerField(min_value=1, max_value=10240)
|
|
||||||
samples = serializers.ListField(child=CreateSampleSerializer())
|
|
||||||
template = serializers.DictField(child=TemplateSerializer())
|
|
||||||
spj = SPJSerializer(allow_null=True)
|
|
||||||
rule_type = serializers.ChoiceField(choices=ProblemRuleType.choices())
|
|
||||||
source = serializers.CharField(max_length=200, allow_blank=True, allow_null=True)
|
|
||||||
answers = serializers.ListField(child=AnswerSerializer())
|
|
||||||
tags = serializers.ListField(child=serializers.CharField())
|
|
||||||
|
|
||||||
|
|
||||||
class FPSProblemSerializer(serializers.Serializer):
|
|
||||||
class UnitSerializer(serializers.Serializer):
|
|
||||||
unit = serializers.ChoiceField(choices=["MB", "s", "ms"])
|
|
||||||
value = serializers.IntegerField(min_value=1, max_value=60000)
|
|
||||||
|
|
||||||
title = serializers.CharField(max_length=128)
|
|
||||||
description = serializers.CharField()
|
|
||||||
input = serializers.CharField()
|
|
||||||
output = serializers.CharField()
|
|
||||||
hint = serializers.CharField(allow_blank=True, allow_null=True)
|
|
||||||
time_limit = UnitSerializer()
|
|
||||||
memory_limit = UnitSerializer()
|
|
||||||
samples = serializers.ListField(child=CreateSampleSerializer())
|
|
||||||
source = serializers.CharField(max_length=200, allow_blank=True, allow_null=True)
|
|
||||||
spj = SPJSerializer(allow_null=True)
|
|
||||||
template = serializers.ListField(
|
|
||||||
child=serializers.DictField(), allow_empty=True, allow_null=True
|
|
||||||
)
|
|
||||||
append = serializers.ListField(
|
|
||||||
child=serializers.DictField(), allow_empty=True, allow_null=True
|
|
||||||
)
|
|
||||||
prepend = serializers.ListField(
|
|
||||||
child=serializers.DictField(), allow_empty=True, allow_null=True
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -20,8 +20,7 @@ from .utils import parse_problem_template
|
|||||||
DEFAULT_PROBLEM_DATA = {"_id": "A-110", "title": "test", "description": "<p>test</p>", "input_description": "test",
|
DEFAULT_PROBLEM_DATA = {"_id": "A-110", "title": "test", "description": "<p>test</p>", "input_description": "test",
|
||||||
"output_description": "test", "time_limit": 1000, "memory_limit": 256, "difficulty": "Low",
|
"output_description": "test", "time_limit": 1000, "memory_limit": 256, "difficulty": "Low",
|
||||||
"visible": True, "tags": ["test"], "languages": ["C", "C++", "Java", "Python2"], "template": {},
|
"visible": True, "tags": ["test"], "languages": ["C", "C++", "Java", "Python2"], "template": {},
|
||||||
"samples": [{"input": "test", "output": "test"}], "spj": False, "spj_language": "C",
|
"samples": [{"input": "test", "output": "test"}], "test_case_id": "499b26290cc7994e0b497212e842ea85",
|
||||||
"spj_code": "", "spj_compile_ok": True, "test_case_id": "499b26290cc7994e0b497212e842ea85",
|
|
||||||
"test_case_score": [{"output_name": "1.out", "input_name": "1.in", "output_size": 0,
|
"test_case_score": [{"output_name": "1.out", "input_name": "1.in", "output_size": 0,
|
||||||
"stripped_output_md5": "d41d8cd98f00b204e9800998ecf8427e",
|
"stripped_output_md5": "d41d8cd98f00b204e9800998ecf8427e",
|
||||||
"input_size": 0, "score": 0}],
|
"input_size": 0, "score": 0}],
|
||||||
@@ -34,14 +33,6 @@ class ProblemCreateTestBase(APITestCase):
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def add_problem(problem_data, created_by):
|
def add_problem(problem_data, created_by):
|
||||||
data = copy.deepcopy(problem_data)
|
data = copy.deepcopy(problem_data)
|
||||||
if data["spj"]:
|
|
||||||
if not data["spj_language"] or not data["spj_code"]:
|
|
||||||
raise ValueError("Invalid spj")
|
|
||||||
data["spj_version"] = hashlib.md5(
|
|
||||||
(data["spj_language"] + ":" + data["spj_code"]).encode("utf-8")).hexdigest()
|
|
||||||
else:
|
|
||||||
data["spj_language"] = None
|
|
||||||
data["spj_code"] = None
|
|
||||||
if data["rule_type"] == ProblemRuleType.OI:
|
if data["rule_type"] == ProblemRuleType.OI:
|
||||||
total_score = 0
|
total_score = 0
|
||||||
for item in data["test_case_score"]:
|
for item in data["test_case_score"]:
|
||||||
@@ -81,12 +72,9 @@ class TestCaseUploadAPITest(APITestCase):
|
|||||||
self.create_super_admin()
|
self.create_super_admin()
|
||||||
|
|
||||||
def test_filter_file_name(self):
|
def test_filter_file_name(self):
|
||||||
self.assertEqual(self.api.filter_name_list(["1.in", "1.out", "2.in", ".DS_Store"], spj=False),
|
self.assertEqual(self.api.filter_name_list(["1.in", "1.out", "2.in", ".DS_Store"]),
|
||||||
["1.in", "1.out"])
|
["1.in", "1.out"])
|
||||||
self.assertEqual(self.api.filter_name_list(["2.in", "2.out"], spj=False), [])
|
self.assertEqual(self.api.filter_name_list(["2.in", "2.out"]), [])
|
||||||
|
|
||||||
self.assertEqual(self.api.filter_name_list(["1.in", "1.out", "2.in"], spj=True), ["1.in", "2.in"])
|
|
||||||
self.assertEqual(self.api.filter_name_list(["2.in", "3.in"], spj=True), [])
|
|
||||||
|
|
||||||
def make_test_case_zip(self):
|
def make_test_case_zip(self):
|
||||||
base_dir = os.path.join("/tmp", "test_case")
|
base_dir = os.path.join("/tmp", "test_case")
|
||||||
@@ -102,27 +90,13 @@ class TestCaseUploadAPITest(APITestCase):
|
|||||||
f.write(os.path.join(base_dir, item), item)
|
f.write(os.path.join(base_dir, item), item)
|
||||||
return zip_file
|
return zip_file
|
||||||
|
|
||||||
def test_upload_spj_test_case_zip(self):
|
|
||||||
with open(self.make_test_case_zip(), "rb") as f:
|
|
||||||
resp = self.client.post(self.url,
|
|
||||||
data={"spj": "true", "file": f}, format="multipart")
|
|
||||||
self.assertSuccess(resp)
|
|
||||||
data = resp.data["data"]
|
|
||||||
self.assertEqual(data["spj"], True)
|
|
||||||
test_case_dir = os.path.join(settings.TEST_CASE_DIR, data["id"])
|
|
||||||
self.assertTrue(os.path.exists(test_case_dir))
|
|
||||||
for item in data["info"]:
|
|
||||||
name = item["input_name"]
|
|
||||||
with open(os.path.join(test_case_dir, name), "r", encoding="utf-8") as f:
|
|
||||||
self.assertEqual(f.read(), name + "\n" + name + "\n" + "end")
|
|
||||||
|
|
||||||
def test_upload_test_case_zip(self):
|
def test_upload_test_case_zip(self):
|
||||||
with open(self.make_test_case_zip(), "rb") as f:
|
with open(self.make_test_case_zip(), "rb") as f:
|
||||||
resp = self.client.post(self.url,
|
resp = self.client.post(self.url,
|
||||||
data={"spj": "false", "file": f}, format="multipart")
|
data={"file": f}, format="multipart")
|
||||||
self.assertSuccess(resp)
|
self.assertSuccess(resp)
|
||||||
data = resp.data["data"]
|
data = resp.data["data"]
|
||||||
self.assertEqual(data["spj"], False)
|
|
||||||
test_case_dir = os.path.join(settings.TEST_CASE_DIR, data["id"])
|
test_case_dir = os.path.join(settings.TEST_CASE_DIR, data["id"])
|
||||||
self.assertTrue(os.path.exists(test_case_dir))
|
self.assertTrue(os.path.exists(test_case_dir))
|
||||||
for item in data["info"]:
|
for item in data["info"]:
|
||||||
@@ -148,16 +122,6 @@ class ProblemAdminAPITest(APITestCase):
|
|||||||
resp = self.client.post(self.url, data=self.data)
|
resp = self.client.post(self.url, data=self.data)
|
||||||
self.assertFailed(resp, "Display ID already exists")
|
self.assertFailed(resp, "Display ID already exists")
|
||||||
|
|
||||||
def test_spj(self):
|
|
||||||
data = copy.deepcopy(self.data)
|
|
||||||
data["spj"] = True
|
|
||||||
|
|
||||||
resp = self.client.post(self.url, data)
|
|
||||||
self.assertFailed(resp, "Invalid spj")
|
|
||||||
|
|
||||||
data["spj_code"] = "test"
|
|
||||||
resp = self.client.post(self.url, data=data)
|
|
||||||
self.assertSuccess(resp)
|
|
||||||
|
|
||||||
def test_get_problem(self):
|
def test_get_problem(self):
|
||||||
self.test_create_problem()
|
self.test_create_problem()
|
||||||
|
|||||||
@@ -1,18 +1,19 @@
|
|||||||
from django.urls import path
|
from django.urls import path
|
||||||
|
|
||||||
from ..views.admin import (ContestProblemAPI, ProblemAPI, TestCaseAPI, MakeContestProblemPublicAPIView,
|
from ..views.admin import (
|
||||||
CompileSPJAPI, AddContestProblemAPI, ExportProblemAPI, ImportProblemAPI,
|
ContestProblemAPI,
|
||||||
FPSProblemImport, ProblemVisibleAPI)
|
ProblemAPI,
|
||||||
|
TestCaseAPI,
|
||||||
|
MakeContestProblemPublicAPIView,
|
||||||
|
AddContestProblemAPI,
|
||||||
|
ProblemVisibleAPI,
|
||||||
|
)
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path("test_case", TestCaseAPI.as_view()),
|
path("test_case", TestCaseAPI.as_view()),
|
||||||
path("compile_spj", CompileSPJAPI.as_view()),
|
|
||||||
path("problem", ProblemAPI.as_view()),
|
path("problem", ProblemAPI.as_view()),
|
||||||
path("problem/visible", ProblemVisibleAPI.as_view()),
|
path("problem/visible", ProblemVisibleAPI.as_view()),
|
||||||
path("contest/problem", ContestProblemAPI.as_view()),
|
path("contest/problem", ContestProblemAPI.as_view()),
|
||||||
path("contest_problem/make_public", MakeContestProblemPublicAPIView.as_view()),
|
path("contest_problem/make_public", MakeContestProblemPublicAPIView.as_view()),
|
||||||
path("contest/add_problem_from_public", AddContestProblemAPI.as_view()),
|
path("contest/add_problem_from_public", AddContestProblemAPI.as_view()),
|
||||||
path("export_problem", ExportProblemAPI.as_view()),
|
|
||||||
path("import_problem", ImportProblemAPI.as_view()),
|
|
||||||
path("import_fps", FPSProblemImport.as_view()),
|
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -3,29 +3,21 @@ import json
|
|||||||
import os
|
import os
|
||||||
|
|
||||||
# import shutil
|
# import shutil
|
||||||
import tempfile
|
|
||||||
import zipfile
|
import zipfile
|
||||||
from wsgiref.util import FileWrapper
|
from wsgiref.util import FileWrapper
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.db import transaction
|
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
from django.http import StreamingHttpResponse, FileResponse
|
from django.http import StreamingHttpResponse
|
||||||
|
|
||||||
from account.decorators import problem_permission_required, ensure_created_by
|
from account.decorators import problem_permission_required, ensure_created_by
|
||||||
from contest.models import Contest, ContestStatus
|
from contest.models import Contest, ContestStatus
|
||||||
from fps.parser import FPSHelper, FPSParser
|
from submission.models import Submission
|
||||||
from judge.dispatcher import SPJCompiler
|
|
||||||
from options.options import SysOptions
|
|
||||||
from submission.models import Submission, JudgeStatus
|
|
||||||
from utils.api import APIView, CSRFExemptAPIView, validate_serializer, APIError
|
from utils.api import APIView, CSRFExemptAPIView, validate_serializer, APIError
|
||||||
from utils.constants import Difficulty
|
|
||||||
from utils.shortcuts import rand_str, natural_sort_key
|
from utils.shortcuts import rand_str, natural_sort_key
|
||||||
from utils.tasks import delete_files
|
|
||||||
from ..models import Problem, ProblemRuleType, ProblemTag
|
from ..models import Problem, ProblemRuleType, ProblemTag
|
||||||
from ..serializers import (
|
from ..serializers import (
|
||||||
CreateContestProblemSerializer,
|
CreateContestProblemSerializer,
|
||||||
CompileSPJSerializer,
|
|
||||||
CreateProblemSerializer,
|
CreateProblemSerializer,
|
||||||
EditProblemSerializer,
|
EditProblemSerializer,
|
||||||
EditContestProblemSerializer,
|
EditContestProblemSerializer,
|
||||||
@@ -34,23 +26,17 @@ from ..serializers import (
|
|||||||
TestCaseUploadForm,
|
TestCaseUploadForm,
|
||||||
ContestProblemMakePublicSerializer,
|
ContestProblemMakePublicSerializer,
|
||||||
AddContestProblemSerializer,
|
AddContestProblemSerializer,
|
||||||
ExportProblemSerializer,
|
|
||||||
ExportProblemRequestSerializer,
|
|
||||||
UploadProblemForm,
|
|
||||||
ImportProblemSerializer,
|
|
||||||
FPSProblemSerializer,
|
|
||||||
)
|
)
|
||||||
from ..utils import TEMPLATE_BASE, build_problem_template
|
|
||||||
|
|
||||||
|
|
||||||
class TestCaseZipProcessor(object):
|
class TestCaseZipProcessor(object):
|
||||||
def process_zip(self, uploaded_zip_file, spj, dir=""):
|
def process_zip(self, uploaded_zip_file, dir=""):
|
||||||
try:
|
try:
|
||||||
zip_file = zipfile.ZipFile(uploaded_zip_file, "r")
|
zip_file = zipfile.ZipFile(uploaded_zip_file, "r")
|
||||||
except zipfile.BadZipFile:
|
except zipfile.BadZipFile:
|
||||||
raise APIError("Bad zip file")
|
raise APIError("Bad zip file")
|
||||||
name_list = zip_file.namelist()
|
name_list = zip_file.namelist()
|
||||||
test_case_list = self.filter_name_list(name_list, spj=spj, dir=dir)
|
test_case_list = self.filter_name_list(name_list, dir=dir)
|
||||||
if not test_case_list:
|
if not test_case_list:
|
||||||
raise APIError("Empty file")
|
raise APIError("Empty file")
|
||||||
|
|
||||||
@@ -69,28 +55,22 @@ class TestCaseZipProcessor(object):
|
|||||||
if item.endswith(".out"):
|
if item.endswith(".out"):
|
||||||
md5_cache[item] = hashlib.md5(content.rstrip()).hexdigest()
|
md5_cache[item] = hashlib.md5(content.rstrip()).hexdigest()
|
||||||
f.write(content)
|
f.write(content)
|
||||||
test_case_info = {"spj": spj, "test_cases": {}}
|
test_case_info = {"test_cases": {}}
|
||||||
|
|
||||||
info = []
|
info = []
|
||||||
|
|
||||||
if spj:
|
# ["1.in", "1.out", "2.in", "2.out"] => [("1.in", "1.out"), ("2.in", "2.out")]
|
||||||
for index, item in enumerate(test_case_list):
|
test_case_list = zip(*[test_case_list[i::2] for i in range(2)])
|
||||||
data = {"input_name": item, "input_size": size_cache[item]}
|
for index, item in enumerate(test_case_list):
|
||||||
info.append(data)
|
data = {
|
||||||
test_case_info["test_cases"][str(index + 1)] = data
|
"stripped_output_md5": md5_cache[item[1]],
|
||||||
else:
|
"input_size": size_cache[item[0]],
|
||||||
# ["1.in", "1.out", "2.in", "2.out"] => [("1.in", "1.out"), ("2.in", "2.out")]
|
"output_size": size_cache[item[1]],
|
||||||
test_case_list = zip(*[test_case_list[i::2] for i in range(2)])
|
"input_name": item[0],
|
||||||
for index, item in enumerate(test_case_list):
|
"output_name": item[1],
|
||||||
data = {
|
}
|
||||||
"stripped_output_md5": md5_cache[item[1]],
|
info.append(data)
|
||||||
"input_size": size_cache[item[0]],
|
test_case_info["test_cases"][str(index + 1)] = data
|
||||||
"output_size": size_cache[item[1]],
|
|
||||||
"input_name": item[0],
|
|
||||||
"output_name": item[1],
|
|
||||||
}
|
|
||||||
info.append(data)
|
|
||||||
test_case_info["test_cases"][str(index + 1)] = data
|
|
||||||
|
|
||||||
with open(os.path.join(test_case_dir, "info"), "w", encoding="utf-8") as f:
|
with open(os.path.join(test_case_dir, "info"), "w", encoding="utf-8") as f:
|
||||||
f.write(json.dumps(test_case_info, indent=4))
|
f.write(json.dumps(test_case_info, indent=4))
|
||||||
@@ -100,29 +80,19 @@ class TestCaseZipProcessor(object):
|
|||||||
|
|
||||||
return info, test_case_id
|
return info, test_case_id
|
||||||
|
|
||||||
def filter_name_list(self, name_list, spj, dir=""):
|
def filter_name_list(self, name_list, dir=""):
|
||||||
ret = []
|
ret = []
|
||||||
prefix = 1
|
prefix = 1
|
||||||
if spj:
|
while True:
|
||||||
while True:
|
in_name = f"{prefix}.in"
|
||||||
in_name = f"{prefix}.in"
|
out_name = f"{prefix}.out"
|
||||||
if f"{dir}{in_name}" in name_list:
|
if f"{dir}{in_name}" in name_list and f"{dir}{out_name}" in name_list:
|
||||||
ret.append(in_name)
|
ret.append(in_name)
|
||||||
prefix += 1
|
ret.append(out_name)
|
||||||
continue
|
prefix += 1
|
||||||
else:
|
continue
|
||||||
return sorted(ret, key=natural_sort_key)
|
else:
|
||||||
else:
|
return sorted(ret, key=natural_sort_key)
|
||||||
while True:
|
|
||||||
in_name = f"{prefix}.in"
|
|
||||||
out_name = f"{prefix}.out"
|
|
||||||
if f"{dir}{in_name}" in name_list and f"{dir}{out_name}" in name_list:
|
|
||||||
ret.append(in_name)
|
|
||||||
ret.append(out_name)
|
|
||||||
prefix += 1
|
|
||||||
continue
|
|
||||||
else:
|
|
||||||
return sorted(ret, key=natural_sort_key)
|
|
||||||
|
|
||||||
|
|
||||||
class TestCaseAPI(CSRFExemptAPIView, TestCaseZipProcessor):
|
class TestCaseAPI(CSRFExemptAPIView, TestCaseZipProcessor):
|
||||||
@@ -145,7 +115,7 @@ class TestCaseAPI(CSRFExemptAPIView, TestCaseZipProcessor):
|
|||||||
test_case_dir = os.path.join(settings.TEST_CASE_DIR, problem.test_case_id)
|
test_case_dir = os.path.join(settings.TEST_CASE_DIR, problem.test_case_id)
|
||||||
if not os.path.isdir(test_case_dir):
|
if not os.path.isdir(test_case_dir):
|
||||||
return self.error("Test case does not exists")
|
return self.error("Test case does not exists")
|
||||||
name_list = self.filter_name_list(os.listdir(test_case_dir), problem.spj)
|
name_list = self.filter_name_list(os.listdir(test_case_dir))
|
||||||
name_list.append("info")
|
name_list.append("info")
|
||||||
file_name = os.path.join(test_case_dir, problem.test_case_id + ".zip")
|
file_name = os.path.join(test_case_dir, problem.test_case_id + ".zip")
|
||||||
with zipfile.ZipFile(file_name, "w") as file:
|
with zipfile.ZipFile(file_name, "w") as file:
|
||||||
@@ -164,7 +134,6 @@ class TestCaseAPI(CSRFExemptAPIView, TestCaseZipProcessor):
|
|||||||
def post(self, request):
|
def post(self, request):
|
||||||
form = TestCaseUploadForm(request.POST, request.FILES)
|
form = TestCaseUploadForm(request.POST, request.FILES)
|
||||||
if form.is_valid():
|
if form.is_valid():
|
||||||
spj = form.cleaned_data["spj"] == "true"
|
|
||||||
file = form.cleaned_data["file"]
|
file = form.cleaned_data["file"]
|
||||||
else:
|
else:
|
||||||
return self.error("Upload failed")
|
return self.error("Upload failed")
|
||||||
@@ -172,39 +141,14 @@ class TestCaseAPI(CSRFExemptAPIView, TestCaseZipProcessor):
|
|||||||
with open(zip_file, "wb") as f:
|
with open(zip_file, "wb") as f:
|
||||||
for chunk in file:
|
for chunk in file:
|
||||||
f.write(chunk)
|
f.write(chunk)
|
||||||
info, test_case_id = self.process_zip(zip_file, spj=spj)
|
info, test_case_id = self.process_zip(zip_file)
|
||||||
os.remove(zip_file)
|
os.remove(zip_file)
|
||||||
return self.success({"id": test_case_id, "info": info, "spj": spj})
|
return self.success({"id": test_case_id, "info": info})
|
||||||
|
|
||||||
|
|
||||||
class CompileSPJAPI(APIView):
|
|
||||||
@validate_serializer(CompileSPJSerializer)
|
|
||||||
def post(self, request):
|
|
||||||
data = request.data
|
|
||||||
spj_version = rand_str(8)
|
|
||||||
error = SPJCompiler(
|
|
||||||
data["spj_code"], spj_version, data["spj_language"]
|
|
||||||
).compile_spj()
|
|
||||||
if error:
|
|
||||||
return self.error(error)
|
|
||||||
else:
|
|
||||||
return self.success()
|
|
||||||
|
|
||||||
|
|
||||||
class ProblemBase(APIView):
|
class ProblemBase(APIView):
|
||||||
def common_checks(self, request):
|
def common_checks(self, request):
|
||||||
data = request.data
|
data = request.data
|
||||||
if data["spj"]:
|
|
||||||
if not data["spj_language"] or not data["spj_code"]:
|
|
||||||
return "Invalid spj"
|
|
||||||
if not data["spj_compile_ok"]:
|
|
||||||
return "SPJ code must be compiled successfully"
|
|
||||||
data["spj_version"] = hashlib.md5(
|
|
||||||
(data["spj_language"] + ":" + data["spj_code"]).encode("utf-8")
|
|
||||||
).hexdigest()
|
|
||||||
else:
|
|
||||||
data["spj_language"] = None
|
|
||||||
data["spj_code"] = None
|
|
||||||
if data["rule_type"] == ProblemRuleType.OI:
|
if data["rule_type"] == ProblemRuleType.OI:
|
||||||
total_score = 0
|
total_score = 0
|
||||||
for item in data["test_case_score"]:
|
for item in data["test_case_score"]:
|
||||||
@@ -529,257 +473,6 @@ class AddContestProblemAPI(APIView):
|
|||||||
return self.success()
|
return self.success()
|
||||||
|
|
||||||
|
|
||||||
class ExportProblemAPI(APIView):
|
|
||||||
def choose_answers(self, user, problem):
|
|
||||||
ret = []
|
|
||||||
for item in problem.languages:
|
|
||||||
submission = (
|
|
||||||
Submission.objects.filter(
|
|
||||||
problem=problem,
|
|
||||||
user_id=user.id,
|
|
||||||
language=item,
|
|
||||||
result=JudgeStatus.ACCEPTED,
|
|
||||||
)
|
|
||||||
.order_by("-create_time")
|
|
||||||
.first()
|
|
||||||
)
|
|
||||||
if submission:
|
|
||||||
ret.append({"language": submission.language, "code": submission.code})
|
|
||||||
return ret
|
|
||||||
|
|
||||||
def process_one_problem(self, zip_file, user, problem, index):
|
|
||||||
info = ExportProblemSerializer(problem).data
|
|
||||||
info["answers"] = self.choose_answers(user, problem=problem)
|
|
||||||
compression = zipfile.ZIP_DEFLATED
|
|
||||||
zip_file.writestr(
|
|
||||||
zinfo_or_arcname=f"{index}/problem.json",
|
|
||||||
data=json.dumps(info, indent=4),
|
|
||||||
compress_type=compression,
|
|
||||||
)
|
|
||||||
problem_test_case_dir = os.path.join(
|
|
||||||
settings.TEST_CASE_DIR, problem.test_case_id
|
|
||||||
)
|
|
||||||
with open(os.path.join(problem_test_case_dir, "info")) as f:
|
|
||||||
info = json.load(f)
|
|
||||||
for k, v in info["test_cases"].items():
|
|
||||||
zip_file.write(
|
|
||||||
filename=os.path.join(problem_test_case_dir, v["input_name"]),
|
|
||||||
arcname=f"{index}/testcase/{v['input_name']}",
|
|
||||||
compress_type=compression,
|
|
||||||
)
|
|
||||||
if not info["spj"]:
|
|
||||||
zip_file.write(
|
|
||||||
filename=os.path.join(problem_test_case_dir, v["output_name"]),
|
|
||||||
arcname=f"{index}/testcase/{v['output_name']}",
|
|
||||||
compress_type=compression,
|
|
||||||
)
|
|
||||||
|
|
||||||
@validate_serializer(ExportProblemRequestSerializer)
|
|
||||||
def get(self, request):
|
|
||||||
problems = Problem.objects.filter(id__in=request.data["problem_id"])
|
|
||||||
for problem in problems:
|
|
||||||
if problem.contest:
|
|
||||||
ensure_created_by(problem.contest, request.user)
|
|
||||||
else:
|
|
||||||
ensure_created_by(problem, request.user)
|
|
||||||
path = f"/tmp/{rand_str()}.zip"
|
|
||||||
with zipfile.ZipFile(path, "w") as zip_file:
|
|
||||||
for index, problem in enumerate(problems):
|
|
||||||
self.process_one_problem(
|
|
||||||
zip_file=zip_file,
|
|
||||||
user=request.user,
|
|
||||||
problem=problem,
|
|
||||||
index=index + 1,
|
|
||||||
)
|
|
||||||
delete_files.send_with_options(args=(path,), delay=300_000)
|
|
||||||
resp = FileResponse(open(path, "rb"))
|
|
||||||
resp["Content-Type"] = "application/zip"
|
|
||||||
resp["Content-Disposition"] = "attachment;filename=problem-export.zip"
|
|
||||||
return resp
|
|
||||||
|
|
||||||
|
|
||||||
class ImportProblemAPI(CSRFExemptAPIView, TestCaseZipProcessor):
|
|
||||||
request_parsers = ()
|
|
||||||
|
|
||||||
def post(self, request):
|
|
||||||
form = UploadProblemForm(request.POST, request.FILES)
|
|
||||||
if form.is_valid():
|
|
||||||
file = form.cleaned_data["file"]
|
|
||||||
tmp_file = f"/tmp/{rand_str()}.zip"
|
|
||||||
with open(tmp_file, "wb") as f:
|
|
||||||
for chunk in file:
|
|
||||||
f.write(chunk)
|
|
||||||
else:
|
|
||||||
return self.error("Upload failed")
|
|
||||||
|
|
||||||
count = 0
|
|
||||||
with zipfile.ZipFile(tmp_file, "r") as zip_file:
|
|
||||||
name_list = zip_file.namelist()
|
|
||||||
for item in name_list:
|
|
||||||
if "/problem.json" in item:
|
|
||||||
count += 1
|
|
||||||
with transaction.atomic():
|
|
||||||
for i in range(1, count + 1):
|
|
||||||
with zip_file.open(f"{i}/problem.json") as f:
|
|
||||||
problem_info = json.load(f)
|
|
||||||
serializer = ImportProblemSerializer(data=problem_info)
|
|
||||||
if not serializer.is_valid():
|
|
||||||
return self.error(
|
|
||||||
f"Invalid problem format, error is {serializer.errors}"
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
problem_info = serializer.data
|
|
||||||
for item in problem_info["template"].keys():
|
|
||||||
if item not in SysOptions.language_names:
|
|
||||||
return self.error(f"Unsupported language {item}")
|
|
||||||
|
|
||||||
problem_info["display_id"] = problem_info["display_id"][:24]
|
|
||||||
for k, v in problem_info["template"].items():
|
|
||||||
problem_info["template"][k] = build_problem_template(
|
|
||||||
v["prepend"], v["template"], v["append"]
|
|
||||||
)
|
|
||||||
|
|
||||||
spj = problem_info["spj"] is not None
|
|
||||||
rule_type = problem_info["rule_type"]
|
|
||||||
test_case_score = problem_info["test_case_score"]
|
|
||||||
|
|
||||||
# process test case
|
|
||||||
_, test_case_id = self.process_zip(
|
|
||||||
tmp_file, spj=spj, dir=f"{i}/testcase/"
|
|
||||||
)
|
|
||||||
|
|
||||||
problem_obj = Problem.objects.create(
|
|
||||||
_id=problem_info["display_id"],
|
|
||||||
title=problem_info["title"],
|
|
||||||
description=problem_info["description"]["value"],
|
|
||||||
input_description=problem_info["input_description"][
|
|
||||||
"value"
|
|
||||||
],
|
|
||||||
output_description=problem_info["output_description"][
|
|
||||||
"value"
|
|
||||||
],
|
|
||||||
hint=problem_info["hint"]["value"],
|
|
||||||
test_case_score=test_case_score if test_case_score else [],
|
|
||||||
time_limit=problem_info["time_limit"],
|
|
||||||
memory_limit=problem_info["memory_limit"],
|
|
||||||
samples=problem_info["samples"],
|
|
||||||
template=problem_info["template"],
|
|
||||||
rule_type=problem_info["rule_type"],
|
|
||||||
source=problem_info["source"],
|
|
||||||
spj=spj,
|
|
||||||
spj_code=problem_info["spj"]["code"] if spj else None,
|
|
||||||
spj_language=problem_info["spj"]["language"]
|
|
||||||
if spj
|
|
||||||
else None,
|
|
||||||
spj_version=rand_str(8) if spj else "",
|
|
||||||
languages=SysOptions.language_names,
|
|
||||||
created_by=request.user,
|
|
||||||
visible=False,
|
|
||||||
difficulty=Difficulty.MID,
|
|
||||||
total_score=sum(item["score"] for item in test_case_score)
|
|
||||||
if rule_type == ProblemRuleType.OI
|
|
||||||
else 0,
|
|
||||||
test_case_id=test_case_id,
|
|
||||||
)
|
|
||||||
for tag_name in problem_info["tags"]:
|
|
||||||
tag_obj, _ = ProblemTag.objects.get_or_create(name=tag_name)
|
|
||||||
problem_obj.tags.add(tag_obj)
|
|
||||||
return self.success({"import_count": count})
|
|
||||||
|
|
||||||
|
|
||||||
class FPSProblemImport(CSRFExemptAPIView):
|
|
||||||
request_parsers = ()
|
|
||||||
|
|
||||||
def _create_problem(self, problem_data, creator):
|
|
||||||
if problem_data["time_limit"]["unit"] == "ms":
|
|
||||||
time_limit = problem_data["time_limit"]["value"]
|
|
||||||
else:
|
|
||||||
time_limit = problem_data["time_limit"]["value"] * 1000
|
|
||||||
template = {}
|
|
||||||
prepend = {}
|
|
||||||
append = {}
|
|
||||||
for t in problem_data["prepend"]:
|
|
||||||
prepend[t["language"]] = t["code"]
|
|
||||||
for t in problem_data["append"]:
|
|
||||||
append[t["language"]] = t["code"]
|
|
||||||
for t in problem_data["template"]:
|
|
||||||
our_lang = lang = t["language"]
|
|
||||||
if lang == "Python":
|
|
||||||
our_lang = "Python3"
|
|
||||||
template[our_lang] = TEMPLATE_BASE.format(
|
|
||||||
prepend.get(lang, ""), t["code"], append.get(lang, "")
|
|
||||||
)
|
|
||||||
spj = problem_data["spj"] is not None
|
|
||||||
Problem.objects.create(
|
|
||||||
_id=f"fps-{rand_str(4)}",
|
|
||||||
title=problem_data["title"],
|
|
||||||
description=problem_data["description"],
|
|
||||||
input_description=problem_data["input"],
|
|
||||||
output_description=problem_data["output"],
|
|
||||||
hint=problem_data["hint"],
|
|
||||||
test_case_score=problem_data["test_case_score"],
|
|
||||||
time_limit=time_limit,
|
|
||||||
memory_limit=problem_data["memory_limit"]["value"],
|
|
||||||
samples=problem_data["samples"],
|
|
||||||
template=template,
|
|
||||||
rule_type=ProblemRuleType.ACM,
|
|
||||||
source=problem_data.get("source", ""),
|
|
||||||
spj=spj,
|
|
||||||
spj_code=problem_data["spj"]["code"] if spj else None,
|
|
||||||
spj_language=problem_data["spj"]["language"] if spj else None,
|
|
||||||
spj_version=rand_str(8) if spj else "",
|
|
||||||
visible=False,
|
|
||||||
languages=SysOptions.language_names,
|
|
||||||
created_by=creator,
|
|
||||||
difficulty=Difficulty.MID,
|
|
||||||
test_case_id=problem_data["test_case_id"],
|
|
||||||
)
|
|
||||||
|
|
||||||
def post(self, request):
|
|
||||||
form = UploadProblemForm(request.POST, request.FILES)
|
|
||||||
if form.is_valid():
|
|
||||||
file = form.cleaned_data["file"]
|
|
||||||
with tempfile.NamedTemporaryFile("wb") as tf:
|
|
||||||
for chunk in file.chunks(4096):
|
|
||||||
tf.file.write(chunk)
|
|
||||||
|
|
||||||
tf.file.flush()
|
|
||||||
os.fsync(tf.file)
|
|
||||||
|
|
||||||
problems = FPSParser(tf.name).parse()
|
|
||||||
else:
|
|
||||||
return self.error("Parse upload file error")
|
|
||||||
|
|
||||||
helper = FPSHelper()
|
|
||||||
with transaction.atomic():
|
|
||||||
for _problem in problems:
|
|
||||||
test_case_id = rand_str()
|
|
||||||
test_case_dir = os.path.join(settings.TEST_CASE_DIR, test_case_id)
|
|
||||||
os.mkdir(test_case_dir)
|
|
||||||
score = []
|
|
||||||
for item in helper.save_test_case(_problem, test_case_dir)[
|
|
||||||
"test_cases"
|
|
||||||
].values():
|
|
||||||
score.append(
|
|
||||||
{
|
|
||||||
"score": 0,
|
|
||||||
"input_name": item["input_name"],
|
|
||||||
"output_name": item.get("output_name"),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
problem_data = helper.save_image(
|
|
||||||
_problem, settings.UPLOAD_DIR, settings.UPLOAD_PREFIX
|
|
||||||
)
|
|
||||||
s = FPSProblemSerializer(data=problem_data)
|
|
||||||
if not s.is_valid():
|
|
||||||
return self.error(f"Parse FPS file error: {s.errors}")
|
|
||||||
problem_data = s.data
|
|
||||||
problem_data["test_case_id"] = test_case_id
|
|
||||||
problem_data["test_case_score"] = score
|
|
||||||
self._create_problem(problem_data, request.user)
|
|
||||||
return self.success({"import_count": len(problems)})
|
|
||||||
|
|
||||||
|
|
||||||
class ProblemVisibleAPI(APIView):
|
class ProblemVisibleAPI(APIView):
|
||||||
@problem_permission_required
|
@problem_permission_required
|
||||||
def put(self, request):
|
def put(self, request):
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ WebSocket consumers for submission updates
|
|||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
from channels.generic.websocket import AsyncWebsocketConsumer
|
from channels.generic.websocket import AsyncWebsocketConsumer
|
||||||
from channels.db import database_sync_to_async
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -73,7 +72,7 @@ class SubmissionConsumer(AsyncWebsocketConsumer):
|
|||||||
|
|
||||||
async def submission_update(self, event):
|
async def submission_update(self, event):
|
||||||
"""
|
"""
|
||||||
接收来自 channel layer 的提交更新消息并发送给客户端
|
接收来自 channel layer 的代码提交更新消息并发送给客户端
|
||||||
这个方法名对应 push_submission_update 中的 type 字段
|
这个方法名对应 push_submission_update 中的 type 字段
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -1,78 +0,0 @@
|
|||||||
from copy import deepcopy
|
|
||||||
from unittest import mock
|
|
||||||
|
|
||||||
from problem.models import Problem, ProblemTag
|
|
||||||
from utils.api.tests import APITestCase
|
|
||||||
from .models import Submission
|
|
||||||
|
|
||||||
DEFAULT_PROBLEM_DATA = {"_id": "A-110", "title": "test", "description": "<p>test</p>", "input_description": "test",
|
|
||||||
"output_description": "test", "time_limit": 1000, "memory_limit": 256, "difficulty": "Low",
|
|
||||||
"visible": True, "tags": ["test"], "languages": ["C", "C++", "Java", "Python2"], "template": {},
|
|
||||||
"samples": [{"input": "test", "output": "test"}], "spj": False, "spj_language": "C",
|
|
||||||
"spj_code": "", "test_case_id": "499b26290cc7994e0b497212e842ea85",
|
|
||||||
"test_case_score": [{"output_name": "1.out", "input_name": "1.in", "output_size": 0,
|
|
||||||
"stripped_output_md5": "d41d8cd98f00b204e9800998ecf8427e",
|
|
||||||
"input_size": 0, "score": 0}],
|
|
||||||
"rule_type": "ACM", "hint": "<p>test</p>", "source": "test"}
|
|
||||||
|
|
||||||
DEFAULT_SUBMISSION_DATA = {
|
|
||||||
"problem_id": "1",
|
|
||||||
"user_id": 1,
|
|
||||||
"username": "test",
|
|
||||||
"code": "xxxxxxxxxxxxxx",
|
|
||||||
"result": -2,
|
|
||||||
"info": {},
|
|
||||||
"language": "C",
|
|
||||||
"statistic_info": {}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
# todo contest submission
|
|
||||||
|
|
||||||
|
|
||||||
class SubmissionPrepare(APITestCase):
|
|
||||||
def _create_problem_and_submission(self):
|
|
||||||
user = self.create_admin("test", "test123", login=False)
|
|
||||||
problem_data = deepcopy(DEFAULT_PROBLEM_DATA)
|
|
||||||
tags = problem_data.pop("tags")
|
|
||||||
problem_data["created_by"] = user
|
|
||||||
self.problem = Problem.objects.create(**problem_data)
|
|
||||||
for tag in tags:
|
|
||||||
tag = ProblemTag.objects.create(name=tag)
|
|
||||||
self.problem.tags.add(tag)
|
|
||||||
self.problem.save()
|
|
||||||
self.submission_data = deepcopy(DEFAULT_SUBMISSION_DATA)
|
|
||||||
self.submission_data["problem_id"] = self.problem.id
|
|
||||||
self.submission = Submission.objects.create(**self.submission_data)
|
|
||||||
|
|
||||||
|
|
||||||
class SubmissionListTest(SubmissionPrepare):
|
|
||||||
def setUp(self):
|
|
||||||
self._create_problem_and_submission()
|
|
||||||
self.create_user("123", "345")
|
|
||||||
self.url = self.reverse("submission_list_api")
|
|
||||||
|
|
||||||
def test_get_submission_list(self):
|
|
||||||
resp = self.client.get(self.url, data={"limit": "10"})
|
|
||||||
self.assertSuccess(resp)
|
|
||||||
|
|
||||||
|
|
||||||
@mock.patch("submission.views.oj.judge_task.send")
|
|
||||||
class SubmissionAPITest(SubmissionPrepare):
|
|
||||||
def setUp(self):
|
|
||||||
self._create_problem_and_submission()
|
|
||||||
self.user = self.create_user("123", "test123")
|
|
||||||
self.url = self.reverse("submission_api")
|
|
||||||
|
|
||||||
def test_create_submission(self, judge_task):
|
|
||||||
resp = self.client.post(self.url, self.submission_data)
|
|
||||||
self.assertSuccess(resp)
|
|
||||||
judge_task.assert_called()
|
|
||||||
|
|
||||||
def test_create_submission_with_wrong_language(self, judge_task):
|
|
||||||
self.submission_data.update({"language": "Python3"})
|
|
||||||
resp = self.client.post(self.url, self.submission_data)
|
|
||||||
self.assertFailed(resp)
|
|
||||||
self.assertDictEqual(resp.data, {"error": "error",
|
|
||||||
"data": "Python3 is now allowed in the problem"})
|
|
||||||
judge_task.assert_not_called()
|
|
||||||
@@ -16,14 +16,6 @@ class LanguageNameChoiceField(serializers.CharField):
|
|||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
class SPJLanguageNameChoiceField(serializers.CharField):
|
|
||||||
def to_internal_value(self, data):
|
|
||||||
data = super().to_internal_value(data)
|
|
||||||
if data and data not in SysOptions.spj_language_names:
|
|
||||||
raise InvalidLanguage(data)
|
|
||||||
return data
|
|
||||||
|
|
||||||
|
|
||||||
class LanguageNameMultiChoiceField(serializers.ListField):
|
class LanguageNameMultiChoiceField(serializers.ListField):
|
||||||
def to_internal_value(self, data):
|
def to_internal_value(self, data):
|
||||||
data = super().to_internal_value(data)
|
data = super().to_internal_value(data)
|
||||||
@@ -31,12 +23,3 @@ class LanguageNameMultiChoiceField(serializers.ListField):
|
|||||||
if item not in SysOptions.language_names:
|
if item not in SysOptions.language_names:
|
||||||
raise InvalidLanguage(item)
|
raise InvalidLanguage(item)
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
class SPJLanguageNameMultiChoiceField(serializers.ListField):
|
|
||||||
def to_internal_value(self, data):
|
|
||||||
data = super().to_internal_value(data)
|
|
||||||
for item in data:
|
|
||||||
if item not in SysOptions.spj_language_names:
|
|
||||||
raise InvalidLanguage(item)
|
|
||||||
return data
|
|
||||||
|
|||||||
@@ -74,6 +74,39 @@ def push_to_user(user_id: int, message_type: str, data: dict):
|
|||||||
logger.error(f"Failed to push message to user {user_id}: error={str(e)}")
|
logger.error(f"Failed to push message to user {user_id}: error={str(e)}")
|
||||||
|
|
||||||
|
|
||||||
|
def push_flowchart_evaluation_update(submission_id: str, user_id: int, data: dict):
|
||||||
|
"""
|
||||||
|
推送流程图评分状态更新到指定用户的 WebSocket 连接
|
||||||
|
|
||||||
|
Args:
|
||||||
|
submission_id: 流程图提交 ID
|
||||||
|
user_id: 用户 ID
|
||||||
|
data: 要发送的数据,应该包含 type, submission_id, score, grade, feedback 等字段
|
||||||
|
"""
|
||||||
|
channel_layer = get_channel_layer()
|
||||||
|
|
||||||
|
if channel_layer is None:
|
||||||
|
logger.warning("Channel layer is not configured, cannot push flowchart evaluation update")
|
||||||
|
return
|
||||||
|
|
||||||
|
# 构建组名,与 SubmissionConsumer 中的组名一致
|
||||||
|
group_name = f"submission_user_{user_id}"
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 向指定用户组发送消息
|
||||||
|
# type 字段对应 consumer 中的方法名(flowchart_evaluation_update)
|
||||||
|
async_to_sync(channel_layer.group_send)(
|
||||||
|
group_name,
|
||||||
|
{
|
||||||
|
"type": "flowchart_evaluation_update", # 对应 SubmissionConsumer.flowchart_evaluation_update 方法
|
||||||
|
"data": data,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
logger.info(f"Pushed flowchart evaluation update: submission_id={submission_id}, user_id={user_id}, type={data.get('type')}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to push flowchart evaluation update: submission_id={submission_id}, user_id={user_id}, error={str(e)}")
|
||||||
|
|
||||||
|
|
||||||
def push_config_update(key: str, value):
|
def push_config_update(key: str, value):
|
||||||
"""
|
"""
|
||||||
推送配置更新到所有连接的客户端
|
推送配置更新到所有连接的客户端
|
||||||
|
|||||||
Reference in New Issue
Block a user