Files
market_page/backend/competition/judge_views.py
jeremygan2021 7695ac3edf
All checks were successful
Deploy to Server / deploy (push) Successful in 18s
逐字稿
2026-03-17 21:43:33 +08:00

551 lines
20 KiB
Python

import json
import logging
import random
import time
import requests
import threading
from django.shortcuts import render, redirect, get_object_or_404
from django.http import JsonResponse, HttpResponse
from django.views.decorators.http import require_http_methods
from django.views.decorators.csrf import csrf_exempt, ensure_csrf_cookie
from django.core.cache import cache
from django.contrib.auth.models import User
from django.conf import settings
from django.db.models import Q, Avg
from django.utils import timezone
import uuid
from .models import Competition, CompetitionEnrollment, Project, Score, ScoreDimension, Comment, ProjectFile
from shop.models import WeChatUser
from shop.sms_utils import send_sms
from ai_services.models import TranscriptionTask
from ai_services.services import AliyunTingwuService
logger = logging.getLogger(__name__)
# --- Helper Functions ---
def get_client_ip(request):
x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
if x_forwarded_for:
ip = x_forwarded_for.split(',')[0]
else:
ip = request.META.get('REMOTE_ADDR')
return ip
def log_audit(request, action, target, result="SUCCESS", details=""):
judge_id = request.session.get('judge_id')
phone = request.session.get('judge_phone', 'Unknown')
role = request.session.get('judge_role', 'unknown')
ip = get_client_ip(request)
timestamp = timezone.now().strftime("%Y-%m-%d %H:%M:%S")
log_entry = f"[{timestamp}] IP:{ip} | Phone:{phone} | Role:{role} | Action:{action} | Target:{target} | Result:{result} | Details:{details}\n"
# Write to a file
try:
with open(settings.BASE_DIR / 'judge_audit.log', 'a', encoding='utf-8') as f:
f.write(log_entry)
except Exception as e:
logger.error(f"Failed to write audit log: {e}")
def judge_required(view_func):
def wrapper(request, *args, **kwargs):
if not request.session.get('judge_id'):
return redirect('judge_login')
return view_func(request, *args, **kwargs)
return wrapper
def check_contestant_access(view_func):
"""
Check if the user is allowed to access.
Contestants have limited access.
"""
def wrapper(request, *args, **kwargs):
if not request.session.get('judge_id'):
return redirect('judge_login')
role = request.session.get('judge_role')
if role == 'contestant':
# Some views might be restricted for contestants
# For now, this decorator just ensures login, but specific views handle logic
pass
return view_func(request, *args, **kwargs)
return wrapper
# --- Views ---
def admin_entry(request):
"""Entry point for /competition/admin"""
if request.session.get('judge_id'):
return redirect('judge_dashboard')
return redirect('judge_login')
@csrf_exempt
def login_view(request):
if request.method == 'GET':
return render(request, 'judge/login.html')
phone = request.POST.get('phone')
code = request.POST.get('code')
if not phone or not code:
return render(request, 'judge/login.html', {'error': '请输入手机号和验证码'})
# Verify Code
cached_code = cache.get(f"sms_code_{phone}")
# Universal pass code for development/testing
if code != cached_code and code != '888888':
return render(request, 'judge/login.html', {'error': '验证码错误 or expired'})
# Check User
try:
user = WeChatUser.objects.filter(phone_number=phone).first()
if not user:
return render(request, 'judge/login.html', {'error': '该手机号未绑定用户'})
# Check roles
# Priority: Judge > Guest > Contestant (if allowed)
is_judge = CompetitionEnrollment.objects.filter(user=user, role='judge').exists()
is_guest = CompetitionEnrollment.objects.filter(user=user, role='guest').exists()
role = None
if is_judge:
role = 'judge'
elif is_guest:
role = 'guest'
else:
# Check if contestant in any competition with allow_contestant_grading=True
contestant_enrollments = CompetitionEnrollment.objects.filter(
user=user,
role='contestant',
competition__allow_contestant_grading=True
)
if contestant_enrollments.exists():
role = 'contestant'
if not role:
return render(request, 'judge/login.html', {'error': '您没有权限登录系统'})
# Login Success
request.session['judge_id'] = user.id
request.session['judge_phone'] = phone
request.session['judge_name'] = user.nickname
request.session['judge_role'] = role
log_audit(request, 'LOGIN', 'System', 'SUCCESS', f"User {user.nickname} logged in as {role}")
return redirect('judge_dashboard')
except Exception as e:
logger.error(f"Login error: {e}")
return render(request, 'judge/login.html', {'error': '系统错误'})
@csrf_exempt
def send_code(request):
if request.method != 'POST':
return JsonResponse({'success': False, 'message': 'Method not allowed'})
try:
data = json.loads(request.body)
phone = data.get('phone')
if not phone or len(phone) != 11:
return JsonResponse({'success': False, 'message': 'Invalid phone number'})
# Generate Code
code = str(random.randint(100000, 999999)) # 6 digits to match typical SMS
cache.set(f"sms_code_{phone}", code, timeout=300) # 5 mins
# Send SMS using the specified API
def _send_async():
try:
api_url = "https://data.tangledup-ai.com/api/send-sms"
payload = {
"phone_number": phone,
"code": code,
"template_code": "SMS_493295002",
"sign_name": "叠加态科技云南",
"additionalProp1": {}
}
headers = {
"Content-Type": "application/json",
"accept": "application/json"
}
response = requests.post(api_url, json=payload, headers=headers, timeout=15)
logger.info(f"SMS Response for {phone}: {response.status_code} - {response.text}")
except Exception as e:
logger.error(f"发送短信异常: {str(e)}")
threading.Thread(target=_send_async).start()
return JsonResponse({'success': True})
except Exception as e:
return JsonResponse({'success': False, 'message': str(e)})
def logout_view(request):
log_audit(request, 'LOGOUT', 'System')
request.session.flush()
return redirect('judge_login')
@judge_required
def dashboard(request):
judge_id = request.session['judge_id']
role = request.session.get('judge_role', 'judge')
user = WeChatUser.objects.get(id=judge_id)
# Get competitions
if role == 'judge':
enrollments = CompetitionEnrollment.objects.filter(user=user, role='judge')
elif role == 'guest':
enrollments = CompetitionEnrollment.objects.filter(user=user, role='guest')
else:
# Contestant: only competitions allowing grading
enrollments = CompetitionEnrollment.objects.filter(
user=user,
role='contestant',
competition__allow_contestant_grading=True
)
competition_ids = enrollments.values_list('competition_id', flat=True)
# Get Projects
projects = Project.objects.filter(
competition_id__in=competition_ids,
status='submitted'
).select_related('contestant__user')
# Format for template
project_list = []
for p in projects:
# Check current score/grading status for this user
# Note: Score model links to 'judge' which is a CompetitionEnrollment
# We need the enrollment for this user in this competition
user_enrollment = enrollments.filter(competition=p.competition).first()
project_list.append({
'id': p.id,
'title': p.title,
'cover_image_url': p.cover_image_url or (p.cover_image.url if p.cover_image else ''),
'contestant_name': p.contestant.user.nickname,
'current_score': p.final_score, # Global score
'status_class': 'status-submitted',
'get_status_display': p.get_status_display()
})
return render(request, 'judge/dashboard.html', {
'projects': project_list,
'user_role': role,
'user_name': request.session.get('judge_name', '用户')
})
@judge_required
def project_detail_api(request, project_id):
judge_id = request.session['judge_id']
role = request.session.get('judge_role', 'judge')
user = WeChatUser.objects.get(id=judge_id)
project = get_object_or_404(Project, id=project_id)
# Check permission
# User must be enrolled in the project's competition with correct role/settings
if role == 'judge':
enrollment = CompetitionEnrollment.objects.filter(user=user, role='judge', competition=project.competition).first()
elif role == 'guest':
enrollment = CompetitionEnrollment.objects.filter(user=user, role='guest', competition=project.competition).first()
else:
enrollment = CompetitionEnrollment.objects.filter(
user=user,
role='contestant',
competition=project.competition,
competition__allow_contestant_grading=True
).first()
if not enrollment:
return JsonResponse({'error': 'No permission'}, status=403)
# Get Dimensions
dimensions = ScoreDimension.objects.filter(competition=project.competition, is_public=True).order_by('order')
# Get existing scores by THIS user
scores = Score.objects.filter(project=project, judge=enrollment)
score_map = {s.dimension_id: s.score for s in scores}
dim_data = []
for d in dimensions:
dim_data.append({
'id': d.id,
'name': d.name,
'weight': float(d.weight),
'max_score': d.max_score,
'current_score': float(score_map.get(d.id, 0))
})
# Get Comments
# If role is contestant, they CANNOT see other people's comments
history = []
current_comment = ""
if role in ['judge', 'guest']:
comments = Comment.objects.filter(project=project).order_by('-created_at')
for c in comments:
history.append({
'judge_name': c.judge.user.nickname,
'content': c.content,
'created_at': c.created_at.strftime("%Y-%m-%d %H:%M")
})
if c.judge.id == enrollment.id:
current_comment = c.content
else:
# Contestant: only see their own comment
my_comment = Comment.objects.filter(project=project, judge=enrollment).first()
if my_comment:
current_comment = my_comment.content
history.append({
'judge_name': user.nickname, # Self
'content': my_comment.content,
'created_at': my_comment.created_at.strftime("%Y-%m-%d %H:%M")
})
# Include AI results
latest_task = TranscriptionTask.objects.filter(project=project, status='SUCCEEDED').order_by('-created_at').first()
ai_data = None
if latest_task:
ai_data = {
'transcription': latest_task.transcription,
'summary': latest_task.summary,
'auto_chapters_data': latest_task.auto_chapters_data,
'transcription_data': latest_task.transcription_data
}
latest_task_any = TranscriptionTask.objects.filter(project=project).order_by('-created_at').first()
audio_url = latest_task_any.file_url if latest_task_any else None
data = {
'id': project.id,
'title': project.title,
'description': project.description,
'contestant_name': project.contestant.user.nickname,
'dimensions': dim_data,
'history_comments': history,
'current_comment': current_comment,
'ai_result': ai_data,
'audio_url': audio_url,
'can_grade': role == 'judge' or (role == 'contestant' and project.contestant.user != user) # Contestant can grade others if allowed
}
# Specifically for guest: can_grade is False
if role == 'guest':
data['can_grade'] = False
return JsonResponse(data)
@judge_required
@csrf_exempt
def submit_score(request):
if request.method != 'POST':
return JsonResponse({'success': False, 'message': 'Method not allowed'})
try:
data = json.loads(request.body)
project_id = data.get('project_id')
comment_content = data.get('comment')
judge_id = request.session['judge_id']
role = request.session.get('judge_role', 'judge')
if role == 'guest':
return JsonResponse({'success': False, 'message': '嘉宾无权评分'})
user = WeChatUser.objects.get(id=judge_id)
project = get_object_or_404(Project, id=project_id)
enrollment = None
if role == 'judge':
enrollment = CompetitionEnrollment.objects.filter(user=user, role='judge', competition=project.competition).first()
else:
enrollment = CompetitionEnrollment.objects.filter(
user=user,
role='contestant',
competition=project.competition,
competition__allow_contestant_grading=True
).first()
if not enrollment:
return JsonResponse({'success': False, 'message': 'No permission'})
# Save Scores
dimensions = ScoreDimension.objects.filter(competition=project.competition, is_public=True)
for d in dimensions:
score_key = f'score_{d.id}'
if score_key in data:
val = data[score_key]
Score.objects.update_or_create(
project=project,
judge=enrollment,
dimension=d,
defaults={'score': val}
)
# Save Comment
if comment_content:
Comment.objects.update_or_create(
project=project,
judge=enrollment,
defaults={'content': comment_content}
)
# Recalculate Project Score
project.calculate_score()
log_audit(request, 'SCORE_UPDATE', f"Project {project.id}", 'SUCCESS')
return JsonResponse({'success': True})
except Exception as e:
logger.error(f"Submit score error: {e}")
return JsonResponse({'success': False, 'message': str(e)})
@judge_required
@csrf_exempt
def upload_audio(request):
# Contestants cannot upload, but Guests can
role = request.session.get('judge_role')
if role not in ['judge', 'guest']:
return JsonResponse({'success': False, 'message': 'Permission denied'})
if request.method != 'POST':
return JsonResponse({'success': False, 'message': 'Method not allowed'})
judge_id = request.session['judge_id']
file_obj = request.FILES.get('file')
project_id = request.POST.get('project_id')
if not file_obj or not project_id:
return JsonResponse({'success': False, 'message': 'Missing file or project_id'})
try:
# Check permission
user = WeChatUser.objects.get(id=judge_id)
project = Project.objects.get(id=project_id)
# Verify judge/guest has access to this project's competition
enrollment = CompetitionEnrollment.objects.filter(
user=user,
role=role,
competition=project.competition
).first()
if not enrollment:
return JsonResponse({'success': False, 'message': 'No permission for this project'})
# Upload to OSS & Create Task
service = AliyunTingwuService()
if not service.bucket:
return JsonResponse({'success': False, 'message': 'OSS not configured'})
file_extension = file_obj.name.split('.')[-1]
file_name = f"transcription/{uuid.uuid4()}.{file_extension}"
oss_url = service.upload_to_oss(file_obj, file_name)
# Create Task Record
task = TranscriptionTask.objects.create(
project=project,
file_url=oss_url,
status=TranscriptionTask.Status.PENDING
)
# Call Tingwu
try:
tingwu_response = service.create_transcription_task(oss_url)
# Handle response format
if 'Data' in tingwu_response and isinstance(tingwu_response['Data'], dict):
task_id = tingwu_response['Data'].get('TaskId')
else:
task_id = tingwu_response.get('TaskId')
if task_id:
task.task_id = task_id
task.status = TranscriptionTask.Status.PROCESSING
task.save()
log_audit(request, 'UPLOAD_AUDIO', f"Task {task.id}", 'SUCCESS')
return JsonResponse({'success': True, 'task_id': task.id, 'file_url': oss_url})
else:
task.status = TranscriptionTask.Status.FAILED
task.error_message = "No TaskId returned"
task.save()
return JsonResponse({'success': False, 'message': 'Failed to create Tingwu task'})
except Exception as e:
task.status = TranscriptionTask.Status.FAILED
task.error_message = str(e)
task.save()
return JsonResponse({'success': False, 'message': f'Tingwu Error: {e}'})
except Exception as e:
logger.error(f"Upload error: {e}")
return JsonResponse({'success': False, 'message': str(e)})
@judge_required
def ai_manage(request):
# Contestants cannot access AI manage
role = request.session.get('judge_role')
if role not in ['judge', 'guest']:
return redirect('judge_dashboard')
judge_id = request.session['judge_id']
user = WeChatUser.objects.get(id=judge_id)
enrollments = CompetitionEnrollment.objects.filter(user=user, role=role)
competition_ids = enrollments.values_list('competition_id', flat=True)
# Get tasks for projects in these competitions
tasks = TranscriptionTask.objects.filter(
project__competition_id__in=competition_ids
).select_related('project').order_by('-created_at')
task_list = []
for t in tasks:
# Get Evaluation Score
# AIEvaluation is linked to Task
evals = t.ai_evaluations.all()
score = evals[0].score if evals else None
task_list.append({
'id': t.id,
'project': t.project,
'file_url': t.file_url,
'file_name': t.file_url.split('/')[-1] if t.file_url else 'Unknown',
'status': t.status,
'status_class': 'status-' + t.status.lower(), # CSS class
'get_status_display': t.get_status_display(),
'ai_score': score
})
return render(request, 'judge/ai_manage.html', {
'tasks': task_list,
'user_name': request.session.get('judge_name', '用户'),
'user_role': role
})
@judge_required
@csrf_exempt
def delete_ai_task(request, task_id):
role = request.session.get('judge_role')
if role not in ['judge', 'guest']:
return JsonResponse({'success': False, 'message': 'Permission denied'})
if request.method != 'POST':
return JsonResponse({'success': False, 'message': 'Method not allowed'})
try:
task = get_object_or_404(TranscriptionTask, id=task_id)
# Permission check
# ...
task.delete()
log_audit(request, 'DELETE_TASK', f"Task {task_id}", 'SUCCESS')
return JsonResponse({'success': True})
except Exception as e:
return JsonResponse({'success': False, 'message': str(e)})