Merge branch 'dev'

* dev: (22 commits)
  修改模板名称
  使用新的生成比赛排名的方式
  JSONField 会自动把字典的 key转换为数字,判断一下,防止重复创建
  fix typo
  晕了,没提交代码就在服务器上调试。。。
  修复用户提交题目的时候,problems_status 中没有对应的 key 导致的错误
  更新用户提交 ac 标志存储方法
  修复 jsonField 中的问题
  增加 model 修改的 migrations
  修改用户题目 ac 状态的存储方式
  更新 ContestRank的生成方法
  不是本人的提交就增加作者用户名
  模板中判断能否分享
  fix typo
  fix typo
  修复 None 导致的 xss filter 错误
  修复None 导致的 xss filter 工作异常
  修复分享代码时候的逻辑错误
  add oj text logo
  增加富文本的 xss 过滤
  ...
This commit is contained in:
virusdefender 2015-09-24 21:36:44 +08:00
commit 4c7f03776b
31 changed files with 636 additions and 222 deletions

View File

@ -4,11 +4,12 @@
文档https://www.zybuluo.com/virusdefender/note/171932
demo: https://qduoj.com
TODO
- 完善文档,目前还差很多
- 完善测试
- 搭建 demo 站点
![oj_previewindex.png][1]

View File

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
import utils.models
class Migration(migrations.Migration):
dependencies = [
('account', '0005_user_problems_status'),
]
operations = [
migrations.AlterField(
model_name='user',
name='problems_status',
field=utils.models.JsonField(default={}),
),
]

View File

@ -2,6 +2,8 @@
from django.db import models
from django.contrib.auth.models import AbstractBaseUser
from jsonfield import JSONField
class AdminGroup(models.Model):
pass
@ -31,7 +33,9 @@ class User(AbstractBaseUser):
# 0代表不是管理员 1是普通管理员 2是超级管理员
admin_type = models.IntegerField(default=0)
# JSON字典用来表示该用户的问题的解决状态 1为ac2为正在进行
problems_status = models.TextField(default="{}")
problems_status = JSONField(default={})
# 找回密码用的token
# reset_password_token = models.CharField(max_length=40, blank=True, null=True)
USERNAME_FIELD = 'username'
REQUIRED_FIELDS = []

View File

@ -46,3 +46,9 @@ class EditUserSerializer(serializers.Serializer):
password = serializers.CharField(max_length=30, min_length=6, required=False, default=None)
email = serializers.EmailField(max_length=254)
admin_type = serializers.IntegerField(default=0)
class ApplyResetPasswordSerializer(serializers.Serializer):
username = serializers.CharField(max_length=30)
email = serializers.EmailField()
captcha = serializers.CharField(max_length=4, min_length=4)

View File

@ -3,17 +3,20 @@ from django import http
from django.contrib import auth
from django.shortcuts import render
from django.db.models import Q
from django.conf import settings
from rest_framework.views import APIView
from rest_framework.response import Response
from utils.shortcuts import serializer_invalid_response, error_response, success_response, paginate
from utils.shortcuts import serializer_invalid_response, error_response, success_response, paginate, rand_str
from utils.captcha import Captcha
from .decorators import login_required
from .models import User
from .serializers import (UserLoginSerializer, UsernameCheckSerializer,
UserRegisterSerializer, UserChangePasswordSerializer,
EmailCheckSerializer, UserSerializer, EditUserSerializer)
EmailCheckSerializer, UserSerializer, EditUserSerializer,
ApplyResetPasswordSerializer)
from .decorators import super_admin_required
class UserLoginAPIView(APIView):
@ -150,6 +153,7 @@ class EmailCheckAPIView(APIView):
class UserAdminAPIView(APIView):
@super_admin_required
def put(self, request):
"""
用户编辑json api接口
@ -181,6 +185,7 @@ class UserAdminAPIView(APIView):
else:
return serializer_invalid_response(serializer)
@super_admin_required
def get(self, request):
"""
用户分页json api接口
@ -222,8 +227,39 @@ class AccountSecurityAPIView(APIView):
username = request.GET.get("username", None)
if username:
try:
User.objects.get(username=username, admin_type__gt=0)
user = User.objects.get(username=username)
except User.DoesNotExist:
return success_response({"applied_captcha": False})
return success_response({"applied_captcha": True})
return success_response({"applied_captcha": True})
if user.admin_type > 0:
return success_response({"applied_captcha": True})
return success_response({"applied_captcha": False})
class ApplyResetPasswordAPIView(APIView):
def post(self, request):
serializer = ApplyResetPasswordSerializer(data=request.data)
if serializer.is_valid():
data = serializer.data
captcha = Captcha(request)
if not captcha.check(data["captcha"]):
return error_response(u"验证码错误")
try:
user = User.objects.get(username=data["username"], email=data["email"])
except User.DoesNotExist:
return error_response(u"用户不存在")
user.reset_password_token = rand_str()
user.save()
# todo
email_template = open(settings.TEMPLATES[0]["DIRS"][0] + "utils/reset_password_email.html", "r").read()
email_template.replace("{{ username }}", user.username).replace("{{ link }}", "/reset_password/?token=" + user.reset_password_token)
return success_response(u"邮件发生成功")
else:
return serializer_invalid_response(serializer)
class ResetPasswordAPIView(APIView):
pass
def user_index_page(request, username):
return render(request, "oj/account/user_index.html")

View File

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
import utils.models
class Migration(migrations.Migration):
dependencies = [
('announcement', '0002_auto_20150818_1445'),
]
operations = [
migrations.AlterField(
model_name='announcement',
name='content',
field=utils.models.RichTextField(),
),
]

View File

@ -3,13 +3,14 @@ from django.db import models
from account.models import User
from group.models import Group
from utils.models import RichTextField
class Announcement(models.Model):
# 标题
title = models.CharField(max_length=50)
# 公告的内容 HTML 格式
content = models.TextField()
content = RichTextField()
# 创建时间
create_time = models.DateTimeField(auto_now_add=True)
# 这个公告是谁创建的

View File

@ -0,0 +1,25 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
import utils.models
class Migration(migrations.Migration):
dependencies = [
('contest', '0009_contestsubmission_first_achieved'),
]
operations = [
migrations.AlterField(
model_name='contest',
name='description',
field=utils.models.RichTextField(),
),
migrations.AlterField(
model_name='contestproblem',
name='description',
field=utils.models.RichTextField(),
),
]

View File

@ -0,0 +1,29 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
from django.conf import settings
import utils.models
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('contest', '0010_auto_20150922_1703'),
]
operations = [
migrations.CreateModel(
name='ContestRank',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('total_submission_number', models.IntegerField(default=0)),
('total_ac_number', models.IntegerField(default=0)),
('total_time', models.IntegerField(default=0)),
('submission_info', utils.models.JsonField(default={})),
('contest', models.ForeignKey(to='contest.Contest')),
('user', models.ForeignKey(to=settings.AUTH_USER_MODEL)),
],
),
]

View File

@ -5,6 +5,10 @@ from django.utils.timezone import now
from account.models import User
from problem.models import AbstractProblem
from group.models import Group
from utils.models import RichTextField
from jsonfield import JSONField
from judge.judger.result import result
GROUP_CONTEST = 0
PUBLIC_CONTEST = 1
@ -17,7 +21,7 @@ CONTEST_UNDERWAY = 0
class Contest(models.Model):
title = models.CharField(max_length=40, unique=True)
description = models.TextField()
description = RichTextField()
# 比赛模式0 即为是acm模式1 即为是按照总的 ac 题目数量排名模式
mode = models.IntegerField()
# 是否显示实时排名结果
@ -102,3 +106,66 @@ class ContestSubmission(models.Model):
class Meta:
db_table = "contest_submission"
class ContestRank(models.Model):
user = models.ForeignKey(User)
contest = models.ForeignKey(Contest)
total_submission_number = models.IntegerField(default=0)
total_ac_number = models.IntegerField(default=0)
# ac 的题目才要加到这个字段里面 = ac 时间 + 错误次数 * 20 * 60
# 没有 ac 的题目不计算罚时 单位是秒
total_time = models.IntegerField(default=0)
# 数据结构{23: {"is_ac": True, "ac_time": 8999, "error_number": 2, "is_first_ac": True}}
# key 是比赛题目的id
submission_info = JSONField(default={})
def update_rank(self, submission):
if not submission.contest_id or submission.contest_id != self.contest_id:
raise ValueError("Error submission type")
# 这道题以前提交过
if str(submission.problem_id) in self.submission_info:
info = self.submission_info[str(submission.problem_id)]
# 如果这道题目已经 ac 了就跳过
if info["is_ac"]:
return
self.total_submission_number += 1
if submission.result == result["accepted"]:
self.total_ac_number += 1
info["is_ac"] = True
info["ac_time"] = (submission.create_time - self.contest.start_time).total_seconds()
# 之前已经提交过,但是是错误的,这次提交是正确的。错误的题目不计入罚时
self.total_time += (info["ac_time"] + info["error_number"] * 20 * 60)
problem = ContestProblem.objects.get(id=submission.problem_id)
if problem.total_accepted_number == 0:
info["is_first_ac"] = True
else:
info["error_number"] += 1
info["is_ac"] = False
else:
# 第一次提交这道题目
self.total_submission_number += 1
info = {"is_ac": False, "ac_time": 0, "error_number": 0, "is_first_ac": False}
if submission.result == result["accepted"]:
self.total_ac_number += 1
info["is_ac"] = True
info["ac_time"] = (submission.create_time - self.contest.start_time).total_seconds()
self.total_time += info["ac_time"]
problem = ContestProblem.objects.get(id=submission.problem_id)
if problem.total_accepted_number == 0:
info["is_first_ac"] = True
else:
info["is_ac"] = False
info["error_number"] = 1
self.submission_info[str(submission.problem_id)] = info
self.save()

View File

@ -16,7 +16,8 @@ from utils.shortcuts import (serializer_invalid_response, error_response,
from account.models import SUPER_ADMIN, User
from account.decorators import login_required
from group.models import Group
from .models import Contest, ContestProblem, ContestSubmission, CONTEST_ENDED, CONTEST_NOT_START, CONTEST_UNDERWAY
from .models import (Contest, ContestProblem, ContestSubmission, CONTEST_ENDED,
CONTEST_NOT_START, CONTEST_UNDERWAY, ContestRank)
from .models import GROUP_CONTEST, PUBLIC_CONTEST, PASSWORD_PROTECTED_CONTEST
from .decorators import check_user_contest_permission
from .serializers import (CreateContestSerializer, ContestSerializer, EditContestSerializer,
@ -334,18 +335,6 @@ def contest_problems_list_page(request, contest_id):
"""
contest = Contest.objects.get(id=contest_id)
contest_problems = ContestProblem.objects.filter(contest=contest).order_by("sort_index")
submissions = ContestSubmission.objects.filter(user=request.user, contest=contest)
state = {}
for item in submissions:
state[item.problem_id] = item.ac
for item in contest_problems:
if item.id in state:
if state[item.id]:
item.state = 1
else:
item.state = 2
else:
item.state = 0
return render(request, "oj/contest/contest_problems_list.html", {"contest_problems": contest_problems,
"contest": {"id": contest_id}})
@ -391,81 +380,16 @@ def contest_list_page(request, page=1):
"keyword": keyword, "join": join})
def _cmp(x, y):
if x["total_ac"] > y["total_ac"]:
return 1
elif x["total_ac"] < y["total_ac"]:
return -1
else:
if x["total_time"] < y["total_time"]:
return 1
else:
return -1
def get_the_formatted_time(seconds):
if not seconds:
return ""
result = str(seconds % 60)
if seconds % 60 < 10:
result = "0" + result
result = str((seconds % 3600) / 60) + ":" + result
if (seconds % 3600) / 60 < 10:
result = "0" + result
result = str(seconds / 3600) + ":" + result
if seconds / 3600 < 10:
result = "0" + result
return result
@check_user_contest_permission
def contest_rank_page(request, contest_id):
contest = Contest.objects.get(id=contest_id)
contest_problems = ContestProblem.objects.filter(contest=contest).order_by("sort_index")
r = redis.Redis(host=REDIS_CACHE["host"], port=REDIS_CACHE["port"], db=REDIS_CACHE["db"])
if contest.real_time_rank:
# 更新rank
result = ContestSubmission.objects.filter(contest=contest).values("user_id"). \
annotate(total_submit=Sum("total_submission_number"))
for i in range(0, len(result)):
# 这个人所有的提交
submissions = ContestSubmission.objects.filter(user_id=result[i]["user_id"], contest_id=contest_id)
result[i]["submissions"] = {}
result[i]["problems"] = []
for problem in contest_problems:
try:
status = submissions.get(problem=problem)
result[i]["problems"].append({
"first_achieved": status.first_achieved,
"ac": status.ac,
"failed_number": status.total_submission_number,
"ac_time": get_the_formatted_time(status.ac_time)})
if status.ac:
result[i]["problems"][-1]["failed_number"] -= 1
except ContestSubmission.DoesNotExist:
result[i]["problems"].append({})
result[i]["total_ac"] = submissions.filter(ac=True).count()
user= User.objects.get(id=result[i]["user_id"])
result[i]["username"] = user.username
result[i]["real_name"] = user.real_name
result[i]["total_time"] = get_the_formatted_time(submissions.filter(ac=True).aggregate(total_time=Sum("total_time"))["total_time"])
result = sorted(result, cmp=_cmp, reverse=True)
r.set("contest_rank_" + contest_id, json.dumps(list(result)))
else:
# 从缓存读取排名信息
result = r.get("contest_rank_" + contest_id)
if result:
result = json.loads(result)
else:
result = []
rank = ContestRank.objects.filter(contest_id=contest_id).order_by("-total_ac_number", "total_time")
return render(request, "oj/contest/contest_rank.html",
{"contest": contest, "contest_problems": contest_problems,
"result": result,
{"rank": rank, "contest": contest,
"contest_problems": contest_problems,
"auto_refresh": request.GET.get("auto_refresh", None) == "true",
"show_real_name": request.GET.get("show_real_name", None) == "true",
"real_time_rank": contest.real_time_rank})
"show_real_name": request.GET.get("show_real_name", None) == "true",})
class ContestTimeAPIView(APIView):
@ -480,5 +404,4 @@ class ContestTimeAPIView(APIView):
return error_response(u"比赛不存在")
return success_response({"start": int((contest.start_time - now()).total_seconds() * 1000),
"end": int((contest.end_time - now()).total_seconds() * 1000),
"status": contest.status})
"status": contest.status})

View File

@ -49,6 +49,13 @@ class ContestSubmissionAPIView(APIView):
judge.delay(submission.id, problem.time_limit, problem.memory_limit, problem.test_case_id)
except Exception:
return error_response(u"提交判题任务失败")
# 修改用户解题状态
problems_status = request.user.problems_status
if "contest_problems" not in problems_status:
problems_status["contest_problems"] = {}
problems_status["contest_problems"][str(data["problem_id"])] = 2
request.user.problems_status = problems_status
request.user.save()
# 增加redis 中判题队列长度的计数器
r = redis.Redis(host=redis_config["host"], port=redis_config["port"], db=redis_config["db"])
r.incr("judge_queue_length")

View File

@ -9,4 +9,5 @@ gunicorn
coverage
django-extensions
supervisor
pillow
pillow
jsonfield

View File

@ -10,7 +10,7 @@ from judge.judger_controller.settings import redis_config
from judge.judger.result import result
from submission.models import Submission
from problem.models import Problem
from contest.models import ContestProblem, Contest, ContestSubmission
from contest.models import ContestProblem, Contest, ContestSubmission, CONTEST_UNDERWAY, ContestRank
from account.models import User
logger = logging.getLogger("app_info")
@ -25,12 +25,20 @@ class MessageQueue(object):
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)
continue
# 更新该用户的解题状态
try:
user = User.objects.get(pk=submission.user_id)
except User.DoesNotExist:
logger.warning("Submission user does not exist, submission_id: " + submission_id)
continue
if submission.result == result["accepted"] and not submission.contest_id:
# 更新普通题目的 ac 计数器
try:
@ -40,15 +48,10 @@ class MessageQueue(object):
except Problem.DoesNotExist:
logger.warning("Submission problem does not exist, submission_id: " + submission_id)
continue
# 更新该用户的解题状态
try:
user = User.objects.get(pk=submission.user_id)
except User.DoesNotExist:
logger.warning("Submission user does not exist, submission_id: " + submission_id)
continue
problems_status = json.loads(user.problems_status)
problems_status[str(problem.id)] = 1
user.problems_status = json.dumps(problems_status)
problems_status = user.problems_status
problems_status["problems"][str(problem.id)] = 1
user.problems_status = problems_status
user.save()
# 普通题目的话,到这里就结束了
@ -57,7 +60,7 @@ class MessageQueue(object):
# 能运行到这里的都是比赛题目
try:
contest = Contest.objects.get(id=submission.contest_id)
if contest.status != 0:
if contest.status != CONTEST_UNDERWAY:
logger.info("Contest debug mode, id: " + str(contest.id) + ", submission id: " + submission_id)
continue
contest_problem = ContestProblem.objects.get(contest=contest, id=submission.problem_id)
@ -68,50 +71,21 @@ class MessageQueue(object):
logger.warning("Submission problem does not exist, submission_id: " + submission_id)
continue
try:
contest_submission = ContestSubmission.objects.get(user_id=submission.user_id, contest=contest,
problem_id=contest_problem.id)
# 提交次数加1
with transaction.atomic():
if submission.result == result["accepted"]:
# 避免这道题已经 ac 了,但是又重新提交了一遍
if not contest_submission.ac:
# 这种情况是这个题目前处于错误状态,就使用已经存储了的罚时加上这道题的实际用时
contest_submission.ac_time = int((submission.create_time - contest.start_time).total_seconds())
contest_submission.total_time += contest_submission.ac_time
contest_submission.total_submission_number += 1
# 标记为已经通过
if contest_problem.total_accepted_number == 0:
contest_submission.first_achieved = True
contest_submission.ac = True
# contest problem ac 计数器加1
contest_problem.total_accepted_number += 1
else:
# 如果这个提交是错误的就罚时20分钟
contest_submission.total_time += 1200
contest_submission.total_submission_number += 1
contest_submission.save()
with transaction.atomic():
try:
contest_rank = ContestRank.objects.get(contest=contest, user=user)
contest_rank.update_rank(submission)
except ContestRank.DoesNotExist:
ContestRank.objects.create(contest=contest, user=user).update_rank(submission)
if submission.result == result["accepted"]:
contest_problem.total_accepted_number += 1
contest_problem.save()
except ContestSubmission.DoesNotExist:
# 第一次提交
with transaction.atomic():
is_ac = submission.result == result["accepted"]
first_achieved = False
ac_time = 0
if is_ac:
ac_time = int((submission.create_time - contest.start_time).total_seconds())
total_time = int((submission.create_time - contest.start_time).total_seconds())
# 增加题目总的ac数计数器
if contest_problem.total_accepted_number == 0:
first_achieved = True
contest_problem.total_accepted_number += 1
contest_problem.save()
else:
# 没过罚时20分钟
total_time = 1200
ContestSubmission.objects.create(user_id=submission.user_id, contest=contest, problem=contest_problem,
ac=is_ac, total_time=total_time, first_achieved=first_achieved,
ac_time=ac_time)
problems_status = user.problems_status
problems_status["contest_problems"][str(contest_problem.id)] = 1
user.problems_status = problems_status
user.save()
logger.debug("Start message queue")
MessageQueue().listen_task()

View File

@ -0,0 +1,9 @@
"""
___ _ _ __ _ _ _
/___\ _ __ | |(_) _ __ ___ \ \ _ _ __| | __ _ ___ | |__ _ _ __ _ __| | _ _
// //| '_ \ | || || '_ \ / _ \ \ \| | | | / _` | / _` | / _ \ | '_ \ | | | | / _` | / _` || | | |
/ \_// | | | || || || | | || __/ /\_/ /| |_| || (_| || (_| || __/ | |_) || |_| | | (_| || (_| || |_| |
\___/ |_| |_||_||_||_| |_| \___| \___/ \__,_| \__,_| \__, | \___| |_.__/ \__, | \__, | \__,_| \__,_|
|___/ |___/ |_|
https://github.com/QingdaoU/OnlineJudge
"""

View File

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
import utils.models
class Migration(migrations.Migration):
dependencies = [
('problem', '0007_remove_problem_last_update_time'),
]
operations = [
migrations.AlterField(
model_name='problem',
name='description',
field=utils.models.RichTextField(),
),
]

View File

@ -2,6 +2,7 @@
from django.db import models
from account.models import User
from utils.models import RichTextField
class ProblemTag(models.Model):
@ -15,7 +16,7 @@ class AbstractProblem(models.Model):
# 标题
title = models.CharField(max_length=50)
# 问题描述 HTML 格式
description = models.TextField()
description = RichTextField()
# 输入描述
input_description = models.CharField(max_length=10000)
# 输出描述

View File

@ -305,15 +305,11 @@ def problem_list_page(request, page=1):
except Exception:
pass
if request.user.is_authenticated():
problems_status = json.loads(request.user.problems_status)
else:
problems_status = {}
# 右侧标签列表 按照关联的题目的数量排序 排除题目数量为0的
tags = ProblemTag.objects.annotate(problem_number=Count("problem")).filter(problem_number__gt=0).order_by("-problem_number")
return render(request, "oj/problem/problem_list.html",
{"problems": current_page, "page": int(page),
"previous_page": previous_page, "next_page": next_page,
"keyword": keyword, "tag": tag_text,"problems_status": problems_status,
"keyword": keyword, "tag": tag_text,
"tags": tags, "difficulty_order": difficulty_order})

View File

@ -218,7 +218,7 @@ require(["jquery", "avalon", "csrfToken", "bsAlert", "editor", "datetimePicker",
description: problem.description,
time_limit: problem.time_limit,
memory_limit: problem.memory_limit,
samples: problem.samples,
samples: [],
test_case_id: problem.test_case_id,
hint: problem.hint,
source: problem.contest.title,
@ -228,6 +228,9 @@ require(["jquery", "avalon", "csrfToken", "bsAlert", "editor", "datetimePicker",
output_description: problem.output_description,
difficulty: 0
};
for (var i = 0; i < problem.samples.length; i++) {
ajaxData.samples.push({input: problem.samples[i].input, output: problem.samples[i].output})
}
$.ajax({
beforeSend: csrfTokenHeader,
url: "/api/admin/problem/",
@ -333,4 +336,4 @@ require(["jquery", "avalon", "csrfToken", "bsAlert", "editor", "datetimePicker",
});
avalon.scan();
});
});

View File

@ -77,20 +77,20 @@ require(["jquery", "avalon", "editor", "uploader", "bsAlert", "csrfToken", "tagE
});
if (avalon.vmodels.editProblem) {
var vm = avalon.vmodels.editProblem;
title: "",
description= "";
timeLimit= -1;
memoryLimit= -1;
samples= [];
hint= "";
visible= true;
difficulty= 0;
inputDescription= "";
outputDescription= "";
testCaseIdd= "";
uploadSuccess= false;
source= "";
testCaseList= [];
vm.title= "",
vm.description= "";
vm.timeLimit= -1;
vm.memoryLimit= -1;
vm.samples= [];
vm.hint= "";
vm.visible= true;
vm.difficulty= 0;
vm.inputDescription= "";
vm.outputDescription= "";
vm.testCaseIdd= "";
vm.uploadSuccess= false;
vm.source= "";
vm.testCaseList= [];
}
else
var vm = avalon.define({

View File

@ -10,8 +10,8 @@ from rest_framework.views import APIView
from judge.judger_controller.tasks import judge
from judge.judger_controller.settings import redis_config
from account.decorators import login_required
from account.models import SUPER_ADMIN, User
from account.decorators import login_required, super_admin_required
from account.models import SUPER_ADMIN, User, REGULAR_USER
from problem.models import Problem
from contest.models import ContestProblem, Contest
@ -53,9 +53,11 @@ class SubmissionAPIView(APIView):
logger.error(e)
return error_response(u"提交判题任务失败")
# 修改用户解题状态
problems_status = json.loads(request.user.problems_status)
problems_status[str(data["problem_id"])] = 2
request.user.problems_status = json.dumps(problems_status)
problems_status = request.user.problems_status
if "problems" not in problems_status:
problems_status["problems"] = {}
problems_status["problems"][str(data["problem_id"])] = 2
request.user.problems_status = problems_status
request.user.save()
# 增加redis 中判题队列长度的计数器
r = redis.Redis(host=redis_config["host"], port=redis_config["port"], db=redis_config["db"])
@ -104,14 +106,17 @@ def _get_submission(submission_id, user):
"""
submission = Submission.objects.get(id=submission_id)
# 超级管理员或者提交者自己或者是一个分享的提交
if user.admin_type == SUPER_ADMIN or submission.user_id == user.id or submission.shared:
return submission
if user.admin_type == SUPER_ADMIN or submission.user_id == user.id:
return {"submission": submission, "can_share": True}
if submission.contest_id:
contest = Contest.objects.get(id=submission.contest_id)
# 比赛提交的话,比赛创建者也可见
if contest.created_by == user:
return submission
raise Submission.DoesNotExist
return {"submission": submission, "can_share": True}
if submission.shared:
return {"submission": submission, "can_share": False}
else:
raise Submission.DoesNotExist
@login_required
@ -120,7 +125,8 @@ def my_submission(request, submission_id):
单个题目的提交详情页
"""
try:
submission = _get_submission(submission_id, request.user)
result = _get_submission(submission_id, request.user)
submission = result["submission"]
except Submission.DoesNotExist:
return error_page(request, u"提交不存在")
@ -143,8 +149,10 @@ def my_submission(request, submission_id):
info = submission.info
else:
info = None
user = User.objects.get(id=submission.user_id)
return render(request, "oj/problem/my_submission.html",
{"submission": submission, "problem": problem, "info": info})
{"submission": submission, "problem": problem, "info": info,
"user": user, "can_share": result["can_share"]})
class SubmissionAdminAPIView(APIView):
@ -222,9 +230,12 @@ class SubmissionShareAPIView(APIView):
if serializer.is_valid():
submission_id = serializer.data["submission_id"]
try:
submission = _get_submission(submission_id, request.user)
result = _get_submission(submission_id, request.user)
except Submission.DoesNotExist:
return error_response(u"提交不存在")
if not result["can_share"]:
return error_page(request, u"提交不存在")
submission = result["submission"]
submission.shared = not submission.shared
submission.save()
return success_response(submission.shared)
@ -233,6 +244,7 @@ class SubmissionShareAPIView(APIView):
class SubmissionRejudgeAdminAPIView(APIView):
@super_admin_required
def post(self, request):
serializer = SubmissionRejudgeSerializer(data=request.data)
if serializer.is_valid():

View File

@ -44,20 +44,7 @@
{% for item in contest_problems %}
<tr>
<th>
<span class="glyphicon
{% if item.state %}
{% ifequal item.state 1%}
glyphicon-ok ac-flag
{% else %}
glyphicon-minus dealing-flag
{% endifequal %}
{% endif %}
"></span>
<span class="{% get_problem_status request.user.problems_status.contest_problems item.id %}"></span>
</th>
<th scope="row">
<a href="/contest/{{ item.contest.id }}/problem/{{ item.id }}/">{{ item.sort_index }}</a>

View File

@ -3,6 +3,7 @@
比赛排名
{% endblock %}
{% block body %}
{% load contest %}
<div class="container main">
<ul class="nav nav-tabs nav-tabs-google contest-tab">
@ -23,13 +24,13 @@
<div class="row">
<div class="col-lg-12">
<h2 class="text-center">排名(
{% if real_time_rank %}
{% if contest.real_time_rank %}
实时
{% else %}
已封榜
{% endif %})
</h2>
{% if result %}
{% if rank %}
<table class="table table-bordered text-center">
<thead>
<tr>
@ -45,24 +46,20 @@
</tr>
</thead>
<tbody class="rank">
{% for item in result %}
{% for item in rank %}
<tr>
<th scope="row">{{ forloop.counter }}</th>
<td>
{{ item.username }}
{{ item.user.username }}
{% if show_real_name %}
{{ item.real_name }}
{% endif %}
</td>
<td>{{ item.total_ac }} / {{ item.total_submit }}</td>
<td>{% if item.total_time %}{{ item.total_time }}{% else %}--{% endif %}</td>
{% for problem in item.problems %}
<td class="
{% if problem %}{% if problem.ac %}{% if problem.first_achieved %}first-achieved{% else %}alert-success{% endif %}{% else %}alert-danger{% endif %}{% endif %}">
{% if problem.ac %}{{ problem.ac_time }}{% endif %}
{% if problem.failed_number %}
(-{{ problem.failed_number }})
{% endif %}
<td>{{ item.total_ac_number }} / {{ item.total_submission_number }}</td>
<td>{% if item.total_time %}{{ item.total_time|format_seconds }}{% else %}--{% endif %}</td>
{% for problem in contest_problems %}
<td class="{% get_submission_class item problem %}">
{% get_submission_content item problem %}
</td>
{% endfor %}
</tr>

View File

@ -40,6 +40,9 @@
{{ submission.result|translate_result }}
</span>
</h4>
{% ifnotequal request.user.id submission.user_id %}
<p>作者:{{ user.username }}</p>
{% endifnotequal %}
{% ifequal submission.result 0 %}
<p>时间 : {{ submission.accepted_answer_time }}ms 语言 :
{{ submission.language|translate_language }}
@ -65,17 +68,18 @@
<div id="code-field">
<textarea id="code-editor">{{ submission.code }}</textarea>
</div>
<div id="share-code" class="col-lg-6 col-md-6">
{% if submission.shared %}
<button class="btn btn-warning" id="share-code-btn">取消分享</button>
{% else %}
<button class="btn btn-primary" id="share-code-btn">分享我的代码</button>
{% endif %}
<textarea class="form-control" id="share-code-textarea"
{% if not submission.shared %}style="display: none" {% endif %}>{{ problem.title }}
{% if can_share %}
<div id="share-code" class="col-lg-6 col-md-6">
{% if submission.shared %}
<button class="btn btn-warning" id="share-code-btn">取消分享</button>
{% else %}
<button class="btn btn-primary" id="share-code-btn">分享我的代码</button>
{% endif %}
<textarea class="form-control" id="share-code-textarea"
{% if not submission.shared %}style="display: none" {% endif %}>{{ problem.title }}
{{ request.build_absolute_uri }}</textarea>
</div>
</div>
{% endif %}
</div>
@ -102,7 +106,7 @@
url: "/api/submission/share/",
method: "post",
data: {submission_id: location.href.split("/")[4]},
success: function(data){
success: function (data) {
location.reload();
}
})

View File

@ -31,7 +31,7 @@
<tbody>
{% for item in problems %}
<tr>
<th><span class="{% get_problem_status problems_status item.id %}"></span></th>
<th><span class="{% get_problem_status request.user.problems_status.problems item.id %}"></span></th>
<th scope="row"><a href="/problem/{{ item.id }}/">{{ item.id }}</a></th>
<td><a href="/problem/{{ item.id }}/">{{ item.title }}</a></td>
<td>

View File

@ -23,6 +23,7 @@ inspect_redis = json.loads(os.popen("docker inspect redis").read())
if not inspect_redis:
print "Error when inspect redis ip"
exit()
redis_ip = inspect_redis[0]["NetworkSettings"]["IPAddress"]
print "redis ip ", redis_ip
@ -31,6 +32,7 @@ inspect_mysql = json.loads(os.popen("docker inspect mysql").read())
if not inspect_mysql:
print "Error when inspect mysql ip"
exit()
mysql_ip = inspect_mysql[0]["NetworkSettings"]["IPAddress"]
print "mysql ip ", mysql_ip
@ -44,6 +46,7 @@ for line in f.readlines():
content += ("\nexport submission_db_host=" + mysql_ip + "\n")
else:
content += line
f.close()

21
utils/models.py Normal file
View File

@ -0,0 +1,21 @@
# coding=utf-8
import json
from django.db import models
from utils.xss_filter import XssHtml
class RichTextField(models.TextField):
__metaclass__ = models.SubfieldBase
def get_prep_value(self, value):
if not value:
value = ""
parser = XssHtml()
parser.feed(value)
parser.close()
return parser.getHtml()
class JsonField(models.TextField):
pass

View File

@ -100,5 +100,5 @@ def paginate(request, query_set, object_serializer):
def rand_str(length=32):
string = hashlib.md5(str(time.time()) + str(random.randrange(1, 9999999900))).hexdigest()
string = hashlib.md5(str(time.time()) + str(random.randrange(1, 987654321234567))).hexdigest()
return string[0:length]

View File

@ -23,9 +23,50 @@ def get_contest_status_color(contest):
return "success"
def get_the_formatted_time(seconds):
if not seconds:
return ""
seconds = int(seconds)
hour = seconds / (60 * 60)
minute = (seconds - hour * 60 * 60) / 60
second = seconds - hour * 60 * 60 - minute * 60
return str(hour) + ":" + str(minute) + ":" + str(second)
def get_submission_class(rank, problem):
if str(problem.id) not in rank.submission_info:
return ""
else:
submission = rank.submission_info[str(problem.id)]
if submission["is_ac"]:
_class = "alert-success"
if submission["is_first_ac"]:
_class += " first-achieved"
else:
_class = "alert-danger"
return _class
def get_submission_content(rank, problem):
if str(problem.id) not in rank.submission_info:
return ""
else:
submission = rank.submission_info[str(problem.id)]
if submission["is_ac"]:
r = get_the_formatted_time(submission["ac_time"])
if submission["error_number"]:
r += "-" + str(submission["error_number"]) + ""
return r
else:
return "-" + str(submission["error_number"]) + ""
from django import template
register = template.Library()
register.filter("contest_status", get_contest_status)
register.filter("contest_status_color", get_contest_status_color)
register.filter("format_seconds", get_the_formatted_time)
register.simple_tag(get_submission_class, name="get_submission_class")
register.simple_tag(get_submission_content, name="get_submission_content")

View File

@ -9,6 +9,9 @@ def get_problem_accepted_radio(problem):
def get_problem_status(problems_status, problem_id):
# 用户没登陆 或者 user.problem_status 中没有这个字段都会到导致这里的problem_status 为 ""
if not problems_status:
return ""
if str(problem_id) in problems_status:
if problems_status[str(problem_id)] == 1:
@ -16,6 +19,7 @@ def get_problem_status(problems_status, problem_id):
return "glyphicon glyphicon-minus dealing-flag"
return ""
from django import template
register = template.Library()

202
utils/xss_filter.py Normal file
View File

@ -0,0 +1,202 @@
# -*- coding: utf-8 -*-
"""
Python 富文本XSS过滤类
@package XssHtml
@version 0.1
@link http://phith0n.github.io/python-xss-filter
@since 20150407
@copyright (c) Phithon All Rights Reserved
Based on native Python module HTMLParser purifier of HTML, To Clear all javascript in html
You can use it in all python web framework
Written by Phithon <root@leavesongs.com> in 2015 and placed in the public domain.
phithon <root@leavesongs.com> 编写于20150407
From: XDSEC <www.xdsec.org> & 离别歌 <www.leavesongs.com>
GitHub Pages: https://github.com/phith0n/python-xss-filter
Usage:
parser = XssHtml()
parser.feed('<html code>')
parser.close()
html = parser.getHtml()
print html
Requirements
Python 2.6+ or 3.2+
Cannot defense xss in browser which is belowed IE7
浏览器版本IE7+ 或其他浏览器无法防御IE6及以下版本浏览器中的XSS
"""
import re
try:
from html.parser import HTMLParser
except:
from HTMLParser import HTMLParser
class XssHtml(HTMLParser):
allow_tags = ['a', 'img', 'br', 'strong', 'b', 'code', 'pre',
'p', 'div', 'em', 'span', 'h1', 'h2', 'h3', 'h4',
'h5', 'h6', 'blockquote', 'ul', 'ol', 'tr', 'th', 'td',
'hr', 'li', 'u', 'embed', 's', 'table', 'thead', 'tbody',
'caption', 'small', 'q', 'sup', 'sub']
common_attrs = ["style", "class", "name"]
nonend_tags = ["img", "hr", "br", "embed"]
tags_own_attrs = {
"img": ["src", "width", "height", "alt", "align"],
"a": ["href", "target", "rel", "title"],
"embed": ["src", "width", "height", "type", "allowfullscreen", "loop", "play", "wmode", "menu"],
"table": ["border", "cellpadding", "cellspacing"],
}
def __init__(self, allows=[]):
HTMLParser.__init__(self)
self.allow_tags = allows if allows else self.allow_tags
self.result = []
self.start = []
self.data = []
def getHtml(self):
"""
Get the safe html code
"""
for i in range(0, len(self.result)):
tmp = self.result[i].rstrip('\n')
tmp = tmp.lstrip('\n')
if tmp:
self.data.append(tmp)
return ''.join(self.data)
def handle_startendtag(self, tag, attrs):
self.handle_starttag(tag, attrs)
def handle_starttag(self, tag, attrs):
if tag not in self.allow_tags:
return
end_diagonal = ' /' if tag in self.nonend_tags else ''
if not end_diagonal:
self.start.append(tag)
attdict = {}
for attr in attrs:
attdict[attr[0]] = attr[1]
attdict = self._wash_attr(attdict, tag)
if hasattr(self, "node_%s" % tag):
attdict = getattr(self, "node_%s" % tag)(attdict)
else:
attdict = self.node_default(attdict)
attrs = []
for (key, value) in attdict.items():
attrs.append('%s="%s"' % (key, self._htmlspecialchars(value)))
attrs = (' ' + ' '.join(attrs)) if attrs else ''
self.result.append('<' + tag + attrs + end_diagonal + '>')
def handle_endtag(self, tag):
if self.start and tag == self.start[len(self.start) - 1]:
self.result.append('</' + tag + '>')
self.start.pop()
def handle_data(self, data):
self.result.append(self._htmlspecialchars(data))
def handle_entityref(self, name):
if name.isalpha():
self.result.append("&%s;" % name)
def handle_charref(self, name):
if name.isdigit():
self.result.append("&#%s;" % name)
def node_default(self, attrs):
attrs = self._common_attr(attrs)
return attrs
def node_a(self, attrs):
attrs = self._common_attr(attrs)
attrs = self._get_link(attrs, "href")
attrs = self._set_attr_default(attrs, "target", "_blank")
attrs = self._limit_attr(attrs, {
"target": ["_blank", "_self"]
})
return attrs
def node_embed(self, attrs):
attrs = self._common_attr(attrs)
attrs = self._get_link(attrs, "src")
attrs = self._limit_attr(attrs, {
"type": ["application/x-shockwave-flash"],
"wmode": ["transparent", "window", "opaque"],
"play": ["true", "false"],
"loop": ["true", "false"],
"menu": ["true", "false"],
"allowfullscreen": ["true", "false"]
})
attrs["allowscriptaccess"] = "never"
attrs["allownetworking"] = "none"
return attrs
def _true_url(self, url):
prog = re.compile(r"^(http|https|ftp)://.+", re.I | re.S)
if prog.match(url):
return url
else:
return "http://%s" % url
def _true_style(self, style):
if style:
style = re.sub(r"(\\|&#|/\*|\*/)", "_", style)
style = re.sub(r"e.*x.*p.*r.*e.*s.*s.*i.*o.*n", "_", style)
return style
def _get_style(self, attrs):
if "style" in attrs:
attrs["style"] = self._true_style(attrs.get("style"))
return attrs
def _get_link(self, attrs, name):
if name in attrs:
attrs[name] = self._true_url(attrs[name])
return attrs
def _wash_attr(self, attrs, tag):
if tag in self.tags_own_attrs:
other = self.tags_own_attrs.get(tag)
else:
other = []
if attrs:
for (key, value) in attrs.items():
if key not in self.common_attrs + other:
del attrs[key]
return attrs
def _common_attr(self, attrs):
attrs = self._get_style(attrs)
return attrs
def _set_attr_default(self, attrs, name, default=''):
if name not in attrs:
attrs[name] = default
return attrs
def _limit_attr(self, attrs, limit={}):
for (key, value) in limit.items():
if key in attrs and attrs[key] not in value:
del attrs[key]
return attrs
def _htmlspecialchars(self, html):
return html.replace("<", "&lt;") \
.replace(">", "&gt;") \
.replace('"', "&quot;") \
.replace("'", "&#039;")
if "__main__" == __name__:
parser = XssHtml()
parser.feed("""<p><img src=1 onerror=alert(/xss/)></p><div class="left">
<a href='javascript:prompt(1)'><br />hehe</a></div>
<p id="test" onmouseover="alert(1)">&gt;M<svg>
<a href="https://www.baidu.com" target="self">MM</a></p>
<embed src='javascript:alert(/hehe/)' allowscriptaccess=always />""")
parser.close()
print(parser.getHtml())