Accept Merge Request #114 创建了单个比赛的详情页等 : (virusdefender-dev -> dev)
Merge Request: 创建了单个比赛的详情页等 Created By: @virusdefender Accepted By: @virusdefender URL: https://coding.net/u/virusdefender/p/qduoj/git/merge/114
This commit is contained in:
@@ -9,19 +9,17 @@ from group.models import Group
|
|||||||
class Contest(models.Model):
|
class Contest(models.Model):
|
||||||
title = models.CharField(max_length=40, unique=True)
|
title = models.CharField(max_length=40, unique=True)
|
||||||
description = models.TextField()
|
description = models.TextField()
|
||||||
# 比赛模式:0 即为是acm模式,1 即为是按照总的 ac 题目数量排名模式,2 即为按照 ac 的题目的总分排名模式
|
# 比赛模式:0 即为是acm模式,1 即为是按照总的 ac 题目数量排名模式
|
||||||
mode = models.IntegerField()
|
mode = models.IntegerField()
|
||||||
# 是否显示排名结果
|
# 是否显示排名结果
|
||||||
show_rank = models.BooleanField()
|
show_rank = models.BooleanField()
|
||||||
# 是否显示别人的提交记录
|
# 是否显示别人的提交记录
|
||||||
show_user_submission = models.BooleanField()
|
show_user_submission = models.BooleanField()
|
||||||
|
|
||||||
# 只能超级管理员创建公开赛,管理员只能创建小组内部的比赛
|
# 只能超级管理员创建公开赛,管理员只能创建小组内部的比赛
|
||||||
# 如果这一项不为空,即为有密码的公开赛,没有密码的可以为小组赛或者是公开赛(此时用比赛的类型来表示)
|
# 如果这一项不为空,即为有密码的公开赛,没有密码的可以为小组赛或者是公开赛(此时用比赛的类型来表示)
|
||||||
password = models.CharField(max_length=30, blank=True, null=True)
|
password = models.CharField(max_length=30, blank=True, null=True)
|
||||||
# 比赛的类型: 0 即为是小组赛,1 即为是无密码的公开赛,2 即为是有密码的公开赛
|
# 比赛的类型: 0 即为是小组赛,1 即为是无密码的公开赛,2 即为是有密码的公开赛
|
||||||
contest_type = models.IntegerField()
|
contest_type = models.IntegerField()
|
||||||
|
|
||||||
# 开始时间
|
# 开始时间
|
||||||
start_time = models.DateTimeField()
|
start_time = models.DateTimeField()
|
||||||
# 结束时间
|
# 结束时间
|
||||||
|
|||||||
@@ -103,3 +103,8 @@ class EditContestProblemSerializer(serializers.Serializer):
|
|||||||
sort_index = serializers.CharField(max_length=30)
|
sort_index = serializers.CharField(max_length=30)
|
||||||
|
|
||||||
|
|
||||||
|
class ContestPasswordVerifySerializer(serializers.Serializer):
|
||||||
|
contest_id = serializers.IntegerField()
|
||||||
|
password = serializers.CharField(max_length=30)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -12,20 +12,15 @@ from utils.shortcuts import (serializer_invalid_response, error_response,
|
|||||||
success_response, paginate, rand_str, error_page)
|
success_response, paginate, rand_str, error_page)
|
||||||
|
|
||||||
from account.models import REGULAR_USER, ADMIN, SUPER_ADMIN
|
from account.models import REGULAR_USER, ADMIN, SUPER_ADMIN
|
||||||
|
from account.decorators import login_required
|
||||||
from group.models import Group
|
from group.models import Group
|
||||||
from announcement.models import Announcement
|
from announcement.models import Announcement
|
||||||
|
|
||||||
from .models import Contest, ContestProblem
|
from .models import Contest, ContestProblem
|
||||||
from .serializers import (CreateContestSerializer, ContestSerializer, EditContestSerializer,
|
from .serializers import (CreateContestSerializer, ContestSerializer, EditContestSerializer,
|
||||||
CreateContestProblemSerializer, ContestProblemSerializer, EditContestProblemSerializer)
|
CreateContestProblemSerializer, ContestProblemSerializer,
|
||||||
|
EditContestProblemSerializer, ContestPasswordVerifySerializer,
|
||||||
|
EditContestProblemSerializer)
|
||||||
def contest_page(request, contest_id):
|
|
||||||
try:
|
|
||||||
contest = Contest.objects.get(id=contest_id, visible=True)
|
|
||||||
except Contest.DoesNotExist:
|
|
||||||
return error_page(request, u"比赛不存在")
|
|
||||||
return render(request, "oj/contest/problems.html", {"contest": contest})
|
|
||||||
|
|
||||||
|
|
||||||
class ContestAdminAPIView(APIView):
|
class ContestAdminAPIView(APIView):
|
||||||
@@ -231,6 +226,61 @@ class ContestProblemAdminAPIView(APIView):
|
|||||||
return paginate(request, contest_problem, ContestProblemSerializer)
|
return paginate(request, contest_problem, ContestProblemSerializer)
|
||||||
|
|
||||||
|
|
||||||
|
class ContestPasswordVerifyAPIView(APIView):
|
||||||
|
@login_required
|
||||||
|
def post(self, request):
|
||||||
|
serializer = ContestPasswordVerifySerializer(data=request.data)
|
||||||
|
if serializer.is_valid():
|
||||||
|
data = request.data
|
||||||
|
try:
|
||||||
|
contest = Contest.objects.get(id=data["contest_id"], contest_type=2)
|
||||||
|
except Contest.DoesNotExist:
|
||||||
|
return error_response(u"密码错误")
|
||||||
|
|
||||||
|
if data["password"] != contest.password:
|
||||||
|
return error_response(u" 密码错误")
|
||||||
|
else:
|
||||||
|
print request.session.get("contests", None)
|
||||||
|
if "contests" not in request.session:
|
||||||
|
request.session["contests"] = []
|
||||||
|
request.session["contests"].append(int(data["contest_id"]))
|
||||||
|
print request.session["contests"]
|
||||||
|
|
||||||
|
return success_response(True)
|
||||||
|
else:
|
||||||
|
return serializer_invalid_response(serializer)
|
||||||
|
|
||||||
|
|
||||||
|
def check_user_contest_permission(request, contest):
|
||||||
|
# 有密码的公开赛
|
||||||
|
if contest.contest_type == 2:
|
||||||
|
# 没有输入过密码
|
||||||
|
if contest.id not in request.session.get("contests", []):
|
||||||
|
return {"result": False, "reason": "password_protect"}
|
||||||
|
|
||||||
|
# 指定小组参加的
|
||||||
|
if contest.contest_type == 0:
|
||||||
|
if not contest.groups.filter(id__in=request.user.group_set.all()).exists():
|
||||||
|
return {"result": False, "reason": "limited_group"}
|
||||||
|
return {"result": True}
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def contest_page(request, contest_id):
|
||||||
|
try:
|
||||||
|
contest = Contest.objects.get(id=contest_id)
|
||||||
|
except Contest.DoesNotExist:
|
||||||
|
return error_page(request, u"比赛不存在")
|
||||||
|
|
||||||
|
Contest.objects.filter(Q(contest_type__in=[1, 2]) | Q(groups__in=request.user.group_set.all()))
|
||||||
|
|
||||||
|
result = check_user_contest_permission(request, contest)
|
||||||
|
if not result["result"]:
|
||||||
|
return render(request, "oj/contest/contest_no_privilege.html", {"contest": contest, "reason": result["reason"]})
|
||||||
|
|
||||||
|
return render(request, "oj/contest/contest_index.html", {"contest": contest})
|
||||||
|
|
||||||
|
|
||||||
def contest_list_page(request, page=1):
|
def contest_list_page(request, page=1):
|
||||||
# 正常情况
|
# 正常情况
|
||||||
contests = Contest.objects.filter(visible=True)
|
contests = Contest.objects.filter(visible=True)
|
||||||
@@ -271,4 +321,4 @@ def contest_list_page(request, page=1):
|
|||||||
{"contests": current_page, "page": int(page),
|
{"contests": current_page, "page": int(page),
|
||||||
"previous_page": previous_page, "next_page": next_page,
|
"previous_page": previous_page, "next_page": next_page,
|
||||||
"keyword": keyword, "announcements": announcements,
|
"keyword": keyword, "announcements": announcements,
|
||||||
"join": join, "now": now})
|
"join": join, "now": now})
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
# coding=utf-8
|
# coding=utf-8
|
||||||
import datetime
|
import json
|
||||||
import redis
|
import redis
|
||||||
import MySQLdb
|
import MySQLdb
|
||||||
import subprocess
|
import subprocess
|
||||||
@@ -38,4 +38,5 @@ def judge(submission_id, time_limit, memory_limit, test_case_id):
|
|||||||
conn.commit()
|
conn.commit()
|
||||||
conn.close()
|
conn.close()
|
||||||
r = redis.Redis(host=redis_config["host"], port=redis_config["port"], db=redis_config["db"])
|
r = redis.Redis(host=redis_config["host"], port=redis_config["port"], db=redis_config["db"])
|
||||||
r.decr("judge_queue_length")
|
r.decr("judge_queue_length")
|
||||||
|
r.lpush("queue", submission_id)
|
||||||
|
|||||||
1
mq/__init__.py
Normal file
1
mq/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
# coding=utf-8
|
||||||
1
mq/models.py
Normal file
1
mq/models.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
# coding=utf-8
|
||||||
1
mq/scripts/__init__.py
Normal file
1
mq/scripts/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
# coding=utf-8
|
||||||
39
mq/scripts/info.py
Normal file
39
mq/scripts/info.py
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
# coding=utf-8
|
||||||
|
import logging
|
||||||
|
import redis
|
||||||
|
from judge.judger_controller.settings import redis_config
|
||||||
|
from judge.judger.result import result
|
||||||
|
from submission.models import Submission
|
||||||
|
from problem.models import Problem
|
||||||
|
|
||||||
|
logger = logging.getLogger("app_info")
|
||||||
|
|
||||||
|
|
||||||
|
class MessageQueue(object):
|
||||||
|
def __init__(self):
|
||||||
|
self.conn = redis.StrictRedis(host=redis_config["host"], port=redis_config["port"], db=redis_config["db"])
|
||||||
|
self.queue = 'queue'
|
||||||
|
|
||||||
|
def listen_task(self):
|
||||||
|
while True:
|
||||||
|
submission_id = self.conn.blpop(self.queue, 0)[1]
|
||||||
|
logger.debug("receive submission_id: " + submission_id)
|
||||||
|
try:
|
||||||
|
submission = Submission.objects.get(id=submission_id)
|
||||||
|
except Submission.DoesNotExist:
|
||||||
|
logger.warning("Submission does not exist, submission_id: " + submission_id)
|
||||||
|
pass
|
||||||
|
|
||||||
|
if submission.result == result["accepted"]:
|
||||||
|
# 更新题目的 ac 计数器
|
||||||
|
try:
|
||||||
|
problem = Problem.objects.get(id=submission.problem_id)
|
||||||
|
problem.total_accepted_number += 1
|
||||||
|
problem.save()
|
||||||
|
except Problem.DoesNotExist:
|
||||||
|
logger.warning("Submission problem does not exist, submission_id: " + submission_id)
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
logger.debug("Start message queue")
|
||||||
|
MessageQueue().listen_task()
|
||||||
@@ -52,8 +52,10 @@ INSTALLED_APPS = (
|
|||||||
'problem',
|
'problem',
|
||||||
'admin',
|
'admin',
|
||||||
'submission',
|
'submission',
|
||||||
|
'mq',
|
||||||
'contest',
|
'contest',
|
||||||
|
|
||||||
|
'django_extensions',
|
||||||
'rest_framework',
|
'rest_framework',
|
||||||
'rest_framework_swagger',
|
'rest_framework_swagger',
|
||||||
)
|
)
|
||||||
@@ -127,10 +129,16 @@ LOGGING = {
|
|||||||
# 日志格式
|
# 日志格式
|
||||||
},
|
},
|
||||||
'handlers': {
|
'handlers': {
|
||||||
'file_handler': {
|
'django_error': {
|
||||||
'level': 'DEBUG',
|
'level': 'DEBUG',
|
||||||
'class': 'logging.handlers.RotatingFileHandler',
|
'class': 'logging.handlers.RotatingFileHandler',
|
||||||
'filename': LOG_PATH + 'info.log',
|
'filename': LOG_PATH + 'django.log',
|
||||||
|
'formatter': 'standard'
|
||||||
|
},
|
||||||
|
'app_info': {
|
||||||
|
'level': 'DEBUG',
|
||||||
|
'class': 'logging.handlers.RotatingFileHandler',
|
||||||
|
'filename': LOG_PATH + 'app_info.log',
|
||||||
'formatter': 'standard'
|
'formatter': 'standard'
|
||||||
},
|
},
|
||||||
'console': {
|
'console': {
|
||||||
@@ -140,13 +148,13 @@ LOGGING = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
'loggers': {
|
'loggers': {
|
||||||
'info_logger': {
|
'app_info': {
|
||||||
'handlers': ['file_handler', "console"],
|
'handlers': ['app_info', "console"],
|
||||||
'level': 'DEBUG',
|
'level': 'DEBUG',
|
||||||
'propagate': True
|
'propagate': True
|
||||||
},
|
},
|
||||||
'django.request': {
|
'django.request': {
|
||||||
'handlers': ['file_handler', 'console'],
|
'handlers': ['django_error', 'console'],
|
||||||
'level': 'DEBUG',
|
'level': 'DEBUG',
|
||||||
'propagate': True,
|
'propagate': True,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ from account.views import (UserLoginAPIView, UsernameCheckAPIView, UserRegisterA
|
|||||||
UserAdminAPIView, UserInfoAPIView)
|
UserAdminAPIView, UserInfoAPIView)
|
||||||
from announcement.views import AnnouncementAdminAPIView
|
from announcement.views import AnnouncementAdminAPIView
|
||||||
|
|
||||||
from contest.views import ContestAdminAPIView, ContestProblemAdminAPIView
|
from contest.views import ContestAdminAPIView, ContestProblemAdminAPIView, ContestPasswordVerifyAPIView
|
||||||
|
|
||||||
from group.views import (GroupAdminAPIView, GroupMemberAdminAPIView,
|
from group.views import (GroupAdminAPIView, GroupMemberAdminAPIView,
|
||||||
JoinGroupAPIView, JoinGroupRequestAdminAPIView)
|
JoinGroupAPIView, JoinGroupRequestAdminAPIView)
|
||||||
@@ -69,5 +69,7 @@ urlpatterns = [
|
|||||||
url(r'^my_submissions/$', "submission.views.my_submission_list_page", name="my_submission_list_page"),
|
url(r'^my_submissions/$', "submission.views.my_submission_list_page", name="my_submission_list_page"),
|
||||||
url(r'^my_submissions/(?P<page>\d+)/$', "submission.views.my_submission_list_page", name="my_submission_list_page"),
|
url(r'^my_submissions/(?P<page>\d+)/$', "submission.views.my_submission_list_page", name="my_submission_list_page"),
|
||||||
url(r'^api/admin/monitor/$', QueueLengthMonitorAPIView.as_view(), name="queue_length_monitor_api"),
|
url(r'^api/admin/monitor/$', QueueLengthMonitorAPIView.as_view(), name="queue_length_monitor_api"),
|
||||||
|
url(r'^contest/(?P<contest_id>\d+)/$', "contest.views.contest_page", name="contest_page"),
|
||||||
|
url(r'^api/contest/password/$', ContestPasswordVerifyAPIView.as_view(), name="contest_password_verify_api"),
|
||||||
|
|
||||||
]
|
]
|
||||||
|
|||||||
24
static/src/js/app/oj/contest/contest_password.js
Normal file
24
static/src/js/app/oj/contest/contest_password.js
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
require(["jquery", "bsAlert", "csrfToken"], function($, bsAlert, csrfTokenHeader){
|
||||||
|
$("#contest-password-btn").click(function(){
|
||||||
|
var password = $("#contest-password").val();
|
||||||
|
if(!password){
|
||||||
|
bsAlert("密码不能为空!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$.ajax({
|
||||||
|
beforeSend: csrfTokenHeader,
|
||||||
|
url: "/api/contest/password/",
|
||||||
|
data: {password: password, contest_id: location.href.split("/")[4]},
|
||||||
|
method: "post",
|
||||||
|
dataType: "json",
|
||||||
|
success: function(data){
|
||||||
|
if(!data.code){
|
||||||
|
location.reload();
|
||||||
|
}
|
||||||
|
else{
|
||||||
|
bsAlert(data.data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
});
|
||||||
@@ -59,6 +59,7 @@ require(["jquery", "codeMirror", "csrfToken", "bsAlert"], function ($, codeMirro
|
|||||||
if(counter++ > 10){
|
if(counter++ > 10){
|
||||||
hideLoading();
|
hideLoading();
|
||||||
bsAlert("抱歉,服务器可能出现了故障,请稍后到我的提交列表中查看");
|
bsAlert("抱歉,服务器可能出现了故障,请稍后到我的提交列表中查看");
|
||||||
|
counter = 0;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
$.ajax({
|
$.ajax({
|
||||||
@@ -73,6 +74,7 @@ require(["jquery", "codeMirror", "csrfToken", "bsAlert"], function ($, codeMirro
|
|||||||
setTimeout(getResult, 1000);
|
setTimeout(getResult, 1000);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
|
counter = 0;
|
||||||
hideLoading();
|
hideLoading();
|
||||||
$("#result").html(getResultHtml(data.data));
|
$("#result").html(getResultHtml(data.data));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -61,18 +61,6 @@ class SubmissionAPIView(APIView):
|
|||||||
submission = Submission.objects.get(id=submission_id, user_id=request.user.id)
|
submission = Submission.objects.get(id=submission_id, user_id=request.user.id)
|
||||||
except Submission.DoesNotExist:
|
except Submission.DoesNotExist:
|
||||||
return error_response(u"提交不存在")
|
return error_response(u"提交不存在")
|
||||||
# 标记这个submission 已经被统计
|
|
||||||
if not submission.is_counted:
|
|
||||||
submission.is_counted = True
|
|
||||||
submission.save()
|
|
||||||
if submission.result == result["accepted"]:
|
|
||||||
# 更新题目的 ac 计数器
|
|
||||||
try:
|
|
||||||
problem = Problem.objects.get(id=submission.problem_id)
|
|
||||||
problem.total_accepted_number += 1
|
|
||||||
problem.save()
|
|
||||||
except Problem.DoesNotExist:
|
|
||||||
pass
|
|
||||||
response_data = {"result": submission.result}
|
response_data = {"result": submission.result}
|
||||||
if submission.result == 0:
|
if submission.result == 0:
|
||||||
response_data["accepted_answer_time"] = submission.accepted_answer_time
|
response_data["accepted_answer_time"] = submission.accepted_answer_time
|
||||||
|
|||||||
38
template/oj/contest/_contest_header.html
Normal file
38
template/oj/contest/_contest_header.html
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
{% load contest %}
|
||||||
|
<h2 class="text-center">{{ contest.title }}</h2>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
<div>
|
||||||
|
<table class="table table-bordered">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>开始时间</th>
|
||||||
|
<th>结束时间</th>
|
||||||
|
<th>状态</th>
|
||||||
|
<th>比赛类型</th>
|
||||||
|
<th>创建者</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>{{ contest.start_time }}</td>
|
||||||
|
<td>{{ contest.end_time }}</td>
|
||||||
|
<td>{{ contest|contest_status }}</td>
|
||||||
|
{% ifequal contest.contest_type 0 %}
|
||||||
|
<td>小组赛</td>
|
||||||
|
{% endifequal %}
|
||||||
|
{% ifequal contest.contest_type 1 %}
|
||||||
|
<td>公开赛</td>
|
||||||
|
{% endifequal %}
|
||||||
|
{% ifequal contest.contest_type 2 %}
|
||||||
|
<td>公开赛(密码保护)</td>
|
||||||
|
{% endifequal %}
|
||||||
|
|
||||||
|
<td>{{ contest.created_by.username }}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<hr>
|
||||||
|
<div>{{ contest.description|safe }}</div>
|
||||||
|
</div>
|
||||||
|
<p class="text-center"></p>
|
||||||
21
template/oj/contest/contest_index.html
Normal file
21
template/oj/contest/contest_index.html
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
{% extends 'oj_base.html' %}
|
||||||
|
|
||||||
|
{% block body %}
|
||||||
|
<div class="container main">
|
||||||
|
<ul class="nav nav-tabs nav-tabs-google">
|
||||||
|
<li role="presentation" class="active">
|
||||||
|
<a href="/contest/{{ contest.id }}/">比赛详情</a>
|
||||||
|
</li>
|
||||||
|
<li role="presentation">
|
||||||
|
<a href="/contest/{{ contest.id }}/problems/">题目</a>
|
||||||
|
</li>
|
||||||
|
<li role="presentation">
|
||||||
|
<a href="/contest/{{ contest.id }}/submissions/">提交</a>
|
||||||
|
</li>
|
||||||
|
<li role="presentation">
|
||||||
|
<a href="/contest/{{ contest.id }}/rank/">排名</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
{% include "oj/contest/_contest_header.html" %}
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
26
template/oj/contest/contest_no_privilege.html
Normal file
26
template/oj/contest/contest_no_privilege.html
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
{% extends 'oj_base.html' %}
|
||||||
|
|
||||||
|
{% block body %}
|
||||||
|
<div class="container main">
|
||||||
|
<ul class="nav nav-tabs nav-tabs-google">
|
||||||
|
<li role="presentation" class="active">
|
||||||
|
<a href="/contest/{{ contest.id }}/">比赛详情</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
{% include "oj/contest/_contest_header.html" %}
|
||||||
|
{% ifequal reason "password_protect" %}
|
||||||
|
<div class="form-inline">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>密码</label>
|
||||||
|
<input type="password" class="form-control" id="contest-password" placeholder="请输入密码">
|
||||||
|
</div>
|
||||||
|
<button type="button" id="contest-password-btn" class="btn btn-primary">提交</button>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="alert alert-danger" role="alert">比赛仅指定小组可以参加,你不在这些小组中。</div>
|
||||||
|
{% endifequal %}
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
{% block js_block %}
|
||||||
|
<script src="/static/js/app/oj/contest/contest_password.js"></script>
|
||||||
|
{% endblock %}
|
||||||
Reference in New Issue
Block a user