add teaching feature
This commit is contained in:
112
CLAUDE.md
Normal file
112
CLAUDE.md
Normal file
@@ -0,0 +1,112 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Project Overview
|
||||
|
||||
**OnlineJudge** is the backend for an Online Judge platform. Built with Django 5 + Django REST Framework, PostgreSQL, Redis, Django Channels (WebSocket), and Dramatiq (async task queue). Python 3.12+, managed with `uv`.
|
||||
|
||||
## Commands
|
||||
|
||||
```bash
|
||||
# Development
|
||||
python dev.py # Start dev server (Daphne ASGI + Django runserver)
|
||||
python manage.py runserver # HTTP only (no WebSocket support)
|
||||
python manage.py migrate # Apply database migrations
|
||||
python manage.py makemigrations # Create new migrations
|
||||
|
||||
# Dependencies
|
||||
uv sync # Install dependencies from uv.lock
|
||||
uv add <package> # Add a dependency
|
||||
|
||||
# Testing
|
||||
python manage.py test # Run Django test suite
|
||||
coverage run manage.py test # Run with coverage
|
||||
coverage report # Show coverage report
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
### App Modules
|
||||
|
||||
Each Django app follows the same structure:
|
||||
```
|
||||
<app>/
|
||||
├── models.py # Django models
|
||||
├── serializers.py # DRF serializers
|
||||
├── views/
|
||||
│ ├── oj.py # User-facing API views
|
||||
│ └── admin.py # Admin API views
|
||||
└── urls/
|
||||
├── oj.py # User-facing URL patterns
|
||||
└── admin.py # Admin URL patterns
|
||||
```
|
||||
|
||||
Apps: `account`, `problem`, `submission`, `contest`, `ai`, `flowchart`, `problemset`, `class_pk`, `announcement`, `tutorial`, `message`, `comment`, `conf`, `options`, `judge`
|
||||
|
||||
### URL Routing
|
||||
|
||||
All routes are registered in `oj/urls.py`:
|
||||
- `api/` — user-facing endpoints
|
||||
- `api/admin/` — admin-only endpoints
|
||||
|
||||
WebSocket routing is in `oj/routing.py`.
|
||||
|
||||
### Settings Structure
|
||||
|
||||
- `oj/settings.py` — base configuration
|
||||
- `oj/dev_settings.py` — development overrides (imported when `OJ_ENV != "production"`)
|
||||
- `oj/production_settings.py` — production overrides
|
||||
|
||||
### Base APIView
|
||||
|
||||
`utils/api/api.py` provides a custom `APIView` base class used by all views. It provides:
|
||||
- `self.success(data)` — returns `{"error": null, "data": data}`
|
||||
- `self.error(msg)` — returns `{"error": "error", "data": msg}`
|
||||
|
||||
All views inherit from this, not Django's generic views or DRF's `APIView` directly.
|
||||
|
||||
### Authentication & Permissions
|
||||
|
||||
`account/decorators.py` provides decorators used on view methods:
|
||||
- `@login_required`
|
||||
- `@super_admin_required`
|
||||
- `@check_contest_permission`
|
||||
|
||||
### Judge System
|
||||
|
||||
- `judge/dispatcher.py` — dispatches submissions to the judge sandbox
|
||||
- `judge/tasks.py` — Dramatiq async tasks for judging
|
||||
- `judge/languages.py` — language configurations (compile/run commands, limits)
|
||||
|
||||
Judge status codes are defined in `utils/constants.py` and must match the frontend's `utils/constants.ts`.
|
||||
|
||||
### WebSocket (Channels)
|
||||
|
||||
`submission/consumers.py` — WebSocket consumer for real-time submission status updates. Uses `channels-redis` as the channel layer backend.
|
||||
|
||||
### Caching
|
||||
|
||||
Redis-backed via `django-redis`. Cache keys use MD5 hashing for consistency. See `utils/cache.py`.
|
||||
|
||||
### AI Integration
|
||||
|
||||
`utils/openai.py` — OpenAI client wrapper configured to work with OpenAI-compatible APIs (e.g., DeepSeek). Used by `ai/` app for submission analysis.
|
||||
|
||||
### Data Directory
|
||||
|
||||
Test cases and submission outputs are stored in a separate data directory (configured in settings, not in the repo). The `data/` directory in the repo contains configuration templates.
|
||||
|
||||
## Key Domain Concepts
|
||||
|
||||
| Concept | Details |
|
||||
|---|---|
|
||||
| Problem types | ACM (binary accept/reject) vs OI (partial scoring) |
|
||||
| Judge statuses | Numeric codes -2 to 9 (defined in `utils/constants.py`) |
|
||||
| User roles | Regular / Admin / Super Admin |
|
||||
| Contest types | Public vs Password Protected |
|
||||
| Supported languages | C, C++, Python2, Python3, Java, JavaScript, Golang, Flowchart |
|
||||
|
||||
## Related Repository
|
||||
|
||||
The frontend is at `D:\Projects\ojnext` — a Vue 3 + Rsbuild project. See its CLAUDE.md for frontend details.
|
||||
@@ -5,6 +5,7 @@ from ..views.oj import (
|
||||
AIDetailDataAPI,
|
||||
AIDurationDataAPI,
|
||||
AIHeatmapDataAPI,
|
||||
AIHintAPI,
|
||||
AILoginSummaryAPI,
|
||||
)
|
||||
|
||||
@@ -12,6 +13,7 @@ urlpatterns = [
|
||||
path("ai/detail", AIDetailDataAPI.as_view()),
|
||||
path("ai/duration", AIDurationDataAPI.as_view()),
|
||||
path("ai/analysis", AIAnalysisAPI.as_view()),
|
||||
path("ai/hint", AIHintAPI.as_view()),
|
||||
path("ai/heatmap", AIHeatmapDataAPI.as_view()),
|
||||
path("ai/login_summary", AILoginSummaryAPI.as_view()),
|
||||
]
|
||||
|
||||
@@ -715,6 +715,86 @@ class AIAnalysisAPI(APIView):
|
||||
return response
|
||||
|
||||
|
||||
class AIHintAPI(APIView):
|
||||
@login_required
|
||||
def post(self, request):
|
||||
submission_id = request.data.get("submission_id")
|
||||
if not submission_id:
|
||||
return self.error("submission_id is required")
|
||||
|
||||
try:
|
||||
submission = Submission.objects.get(id=submission_id, user_id=request.user.id)
|
||||
except Submission.DoesNotExist:
|
||||
return self.error("Submission not found")
|
||||
|
||||
problem = submission.problem
|
||||
client = get_ai_client()
|
||||
|
||||
# 获取参考答案(同语言优先,否则取第一个)
|
||||
answers = problem.answers or []
|
||||
ref_answer = next(
|
||||
(a["code"] for a in answers if a["language"] == submission.language),
|
||||
answers[0]["code"] if answers else "",
|
||||
)
|
||||
|
||||
system_prompt = (
|
||||
"你是编程助教。你知道题目的参考答案,但【绝对禁止】把参考答案或其中任何代码"
|
||||
"直接告诉学生,也不能以任何形式暗示完整解法。"
|
||||
"你的任务是:对照参考答案,找出学生代码中的问题,"
|
||||
"给出方向性提示(例如:指出哪类边界情况需要考虑、"
|
||||
"哪个算法思路更合适、哪行代码逻辑可能有问题等)。"
|
||||
"语气鼓励,回复简洁(3-5句话),使用 Markdown 格式。"
|
||||
)
|
||||
user_prompt = (
|
||||
f"题目:{problem.title}\n"
|
||||
f"题目描述:{problem.description[:500]}\n"
|
||||
f"参考答案(仅供你分析,不可透露给学生):\n```\n{ref_answer[:2000]}\n```\n"
|
||||
f"学生提交语言:{submission.language}\n"
|
||||
f"判题结果:{submission.result}\n"
|
||||
f"错误信息:{submission.statistic_info.get('err_info', '无')}\n"
|
||||
f"学生代码:\n```\n{submission.code[:2000]}\n```"
|
||||
)
|
||||
|
||||
def stream_generator():
|
||||
try:
|
||||
stream = client.chat.completions.create(
|
||||
model="deepseek-chat",
|
||||
messages=[
|
||||
{"role": "system", "content": system_prompt},
|
||||
{"role": "user", "content": user_prompt},
|
||||
],
|
||||
stream=True,
|
||||
)
|
||||
except Exception as exc:
|
||||
yield f"data: {json.dumps({'type': 'error', 'message': str(exc)})}\n\n"
|
||||
yield "event: end\n\n"
|
||||
return
|
||||
|
||||
yield "event: start\n\n"
|
||||
try:
|
||||
for chunk in stream:
|
||||
if not chunk.choices:
|
||||
continue
|
||||
choice = chunk.choices[0]
|
||||
if choice.finish_reason:
|
||||
yield f"data: {json.dumps({'type': 'done'})}\n\n"
|
||||
break
|
||||
content = choice.delta.content
|
||||
if content:
|
||||
yield f"data: {json.dumps({'type': 'delta', 'content': content})}\n\n"
|
||||
except Exception as exc:
|
||||
yield f"data: {json.dumps({'type': 'error', 'message': str(exc)})}\n\n"
|
||||
finally:
|
||||
yield "event: end\n\n"
|
||||
|
||||
response = StreamingHttpResponse(
|
||||
streaming_content=stream_generator(),
|
||||
content_type="text/event-stream",
|
||||
)
|
||||
response["Cache-Control"] = "no-cache"
|
||||
return response
|
||||
|
||||
|
||||
class AIHeatmapDataAPI(APIView):
|
||||
@login_required
|
||||
def get(self, request):
|
||||
|
||||
@@ -4,6 +4,7 @@ from ..views.admin import (
|
||||
ContestProblemAPI,
|
||||
ProblemAPI,
|
||||
ProblemFlowchartAIGen,
|
||||
StuckProblemsAPI,
|
||||
TestCaseAPI,
|
||||
MakeContestProblemPublicAPIView,
|
||||
AddContestProblemAPI,
|
||||
@@ -14,6 +15,7 @@ urlpatterns = [
|
||||
path("test_case", TestCaseAPI.as_view()),
|
||||
path("problem", ProblemAPI.as_view()),
|
||||
path("problem/visible", ProblemVisibleAPI.as_view()),
|
||||
path("problem/stuck", StuckProblemsAPI.as_view()),
|
||||
path("problem/flowchart", ProblemFlowchartAIGen.as_view()),
|
||||
path("contest/problem", ContestProblemAPI.as_view()),
|
||||
path("contest_problem/make_public", MakeContestProblemPublicAPIView.as_view()),
|
||||
|
||||
@@ -7,12 +7,14 @@ from ..views.oj import (
|
||||
ContestProblemAPI,
|
||||
PickOneAPI,
|
||||
ProblemAuthorAPI,
|
||||
SimilarProblemAPI,
|
||||
)
|
||||
|
||||
urlpatterns = [
|
||||
path("problem/tags", ProblemTagAPI.as_view()),
|
||||
path("problem", ProblemAPI.as_view()),
|
||||
path("problem/beat_count", ProblemSolvedPeopleCount.as_view()),
|
||||
path("problem/similar", SimilarProblemAPI.as_view()),
|
||||
path("problem/author", ProblemAuthorAPI.as_view()),
|
||||
path("pickone", PickOneAPI.as_view()),
|
||||
path("contest/problem", ContestProblemAPI.as_view()),
|
||||
|
||||
@@ -10,7 +10,9 @@ from django.conf import settings
|
||||
from django.db.models import Q
|
||||
from django.http import StreamingHttpResponse
|
||||
|
||||
from account.decorators import problem_permission_required, ensure_created_by
|
||||
from django.db.models import Count
|
||||
|
||||
from account.decorators import problem_permission_required, ensure_created_by, super_admin_required
|
||||
from contest.models import Contest, ContestStatus
|
||||
from submission.models import Submission
|
||||
from utils.api import APIView, CSRFExemptAPIView, validate_serializer, APIError
|
||||
@@ -509,3 +511,33 @@ class ProblemFlowchartAIGen(APIView):
|
||||
|
||||
mermaid_code = response.choices[0].message.content
|
||||
return self.success({"flowchart": mermaid_code})
|
||||
|
||||
|
||||
class StuckProblemsAPI(APIView):
|
||||
@super_admin_required
|
||||
def get(self, request):
|
||||
rows = (
|
||||
Submission.objects.values("problem_id", "problem___id", "problem__title")
|
||||
.annotate(
|
||||
total=Count("id"),
|
||||
accepted=Count("id", filter=Q(result=0)),
|
||||
failed=Count("id", filter=Q(result__lt=0)),
|
||||
failed_users=Count("user_id", filter=Q(result__lt=0), distinct=True),
|
||||
)
|
||||
.filter(failed_users__gt=0)
|
||||
.order_by("-failed_users")[:30]
|
||||
)
|
||||
result = [
|
||||
{
|
||||
"problem_id": r["problem___id"],
|
||||
"problem_title": r["problem__title"],
|
||||
"total": r["total"],
|
||||
"failed": r["failed"],
|
||||
"failed_users": r["failed_users"],
|
||||
"ac_rate": round(r["accepted"] / r["total"] * 100, 1)
|
||||
if r["total"]
|
||||
else 0,
|
||||
}
|
||||
for r in rows
|
||||
]
|
||||
return self.success(result)
|
||||
|
||||
@@ -63,6 +63,22 @@ class ProblemAPI(APIView):
|
||||
)
|
||||
problem_data = ProblemSerializer(problem).data
|
||||
self._add_problem_status(request, problem_data)
|
||||
if request.user.is_authenticated:
|
||||
failed_statuses = [
|
||||
JudgeStatus.WRONG_ANSWER,
|
||||
JudgeStatus.CPU_TIME_LIMIT_EXCEEDED,
|
||||
JudgeStatus.REAL_TIME_LIMIT_EXCEEDED,
|
||||
JudgeStatus.MEMORY_LIMIT_EXCEEDED,
|
||||
JudgeStatus.RUNTIME_ERROR,
|
||||
JudgeStatus.COMPILE_ERROR,
|
||||
]
|
||||
problem_data["my_failed_count"] = Submission.objects.filter(
|
||||
user_id=request.user.id,
|
||||
problem_id=problem.id,
|
||||
result__in=failed_statuses,
|
||||
).count()
|
||||
else:
|
||||
problem_data["my_failed_count"] = 0
|
||||
return self.success(problem_data)
|
||||
except Problem.DoesNotExist:
|
||||
return self.error("Problem does not exist")
|
||||
@@ -187,6 +203,40 @@ class ProblemSolvedPeopleCount(APIView):
|
||||
return self.success(rate)
|
||||
|
||||
|
||||
class SimilarProblemAPI(APIView):
|
||||
def get(self, request):
|
||||
problem_display_id = request.GET.get("problem_id")
|
||||
if not problem_display_id:
|
||||
return self.error("problem_id is required")
|
||||
|
||||
try:
|
||||
problem = Problem.objects.get(_id=problem_display_id, contest__isnull=True)
|
||||
except Problem.DoesNotExist:
|
||||
return self.error("Problem not found")
|
||||
|
||||
tag_ids = list(problem.tags.values_list("id", flat=True))
|
||||
if not tag_ids:
|
||||
return self.success([])
|
||||
|
||||
exclude_ids = [problem_display_id]
|
||||
if request.user.is_authenticated:
|
||||
profile = request.user.userprofile
|
||||
ac_display_ids = [
|
||||
v["_id"]
|
||||
for v in profile.acm_problems_status.get("problems", {}).values()
|
||||
if v.get("status") == JudgeStatus.ACCEPTED
|
||||
]
|
||||
exclude_ids.extend(ac_display_ids)
|
||||
|
||||
similar = (
|
||||
Problem.objects.filter(tags__in=tag_ids, visible=True, contest__isnull=True)
|
||||
.exclude(_id__in=exclude_ids)
|
||||
.distinct()
|
||||
.order_by("difficulty")[:5]
|
||||
)
|
||||
return self.success(ProblemListSerializer(similar, many=True).data)
|
||||
|
||||
|
||||
class ProblemAuthorAPI(APIView):
|
||||
def get(self, request):
|
||||
show_all = request.GET.get("all", "0") == "1"
|
||||
|
||||
Reference in New Issue
Block a user