diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..b6cb82c9 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,3 @@ +static/src/js/lib/* linguist-vendored +static/src/js/require.js linguist-vendored +static/src/js/r.js linguist-vendored \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..7126efb1 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2015 Qingdao 青岛大学信息工程学院创新实验室 + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index e69de29b..cdf478d7 100644 --- a/README.md +++ b/README.md @@ -0,0 +1,33 @@ +# OnlineJudge + +基于 Python 和 Django的在线评测平台。 + +文档:https://www.zybuluo.com/virusdefender/note/171932 + +TODO: + + - 完善文档,目前还差很多 + - 完善测试 + - 搭建 demo 站点 + +![oj_previewindex.png][1] + +![preview.jpeg][2] + +![oj_preview_submission.png][3] + +![contest][4] + +![contest_rank_edit][5] + +![admin_problem][6] + +![admin_contest][7] + +[1]: https://dn-virusdefender-blog-cdn.qbox.me/oj_previewindex.png +[2]: https://dn-virusdefender-blog-cdn.qbox.me/oj_previewproblem.png +[3]: https://dn-virusdefender-blog-cdn.qbox.me/oj_previewsubmission.png +[4]: https://dn-virusdefender-blog-cdn.qbox.me/oj_previewcontest.png +[5]: https://dn-virusdefender-blog-cdn.qbox.me/oj_previewcontest_rank.png?edit +[6]: https://dn-virusdefender-blog-cdn.qbox.me/oj_previewadmin_problem.png +[7]: https://dn-virusdefender-blog-cdn.qbox.me/oj_previewadmin_contest.png \ No newline at end of file diff --git a/account/migrations/0002_user_problems_status.py b/account/migrations/0002_user_problems_status.py new file mode 100644 index 00000000..58c8a31c --- /dev/null +++ b/account/migrations/0002_user_problems_status.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('account', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='user', + name='problems_status', + field=models.TextField(blank=True), + ), + ] diff --git a/account/models.py b/account/models.py index df25329c..754e178d 100644 --- a/account/models.py +++ b/account/models.py @@ -30,6 +30,9 @@ class User(AbstractBaseUser): create_time = models.DateTimeField(auto_now_add=True) # 0代表不是管理员 1是普通管理员 2是超级管理员 admin_type = models.IntegerField(default=0) + # JSON字典用来表示该用户的问题的解决状态 1为ac,2为正在进行 + problems_status = models.TextField(blank=True) + USERNAME_FIELD = 'username' REQUIRED_FIELDS = [] diff --git a/contest/views.py b/contest/views.py index 31a6c36c..5bb089bc 100644 --- a/contest/views.py +++ b/contest/views.py @@ -433,7 +433,9 @@ def contest_rank_page(request, contest_id): except ContestSubmission.DoesNotExist: result[i]["problems"].append({}) result[i]["total_ac"] = submissions.filter(ac=True).count() - result[i]["username"] = User.objects.get(id=result[i]["user_id"]).username + 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_time_format(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))) @@ -449,4 +451,5 @@ def contest_rank_page(request, contest_id): {"contest": contest, "contest_problems": contest_problems, "result": result, "auto_refresh": request.GET.get("auto_refresh", None) == "true", + "show_real_name": result.GET.get("show_real_name", None) == "true", "real_time_rank": contest.real_time_rank}) diff --git a/install/__init__.py b/install/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/install/migrations/__init__.py b/install/migrations/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/install/models.py b/install/models.py deleted file mode 100644 index 71a83623..00000000 --- a/install/models.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.db import models - -# Create your models here. diff --git a/install/tests.py b/install/tests.py deleted file mode 100644 index 7ce503c2..00000000 --- a/install/tests.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.test import TestCase - -# Create your tests here. diff --git a/install/views.py b/install/views.py deleted file mode 100644 index 75fd7bd5..00000000 --- a/install/views.py +++ /dev/null @@ -1,23 +0,0 @@ -# coding=utf-8 -from django.shortcuts import render -from django.http import HttpResponse - -from account.models import User -from group.models import Group, UserGroupRelation, JoinGroupRequest - - -def install(request): - for i in range(10): - user = User.objects.create(username="root" + str(i), admin_type=2, real_name="real_name", email="11111@qq.com") - user.set_password("root") - user.save() - for i in range(10): - group = Group.objects.create(name="group" + str(i), - description="description", - admin=User.objects.get(username="root0")) - for i in range(7): - UserGroupRelation.objects.create(user=User.objects.get(username="root" + str(i)), group=group) - for i in range(7, 10): - JoinGroupRequest.objects.create(user=User.objects.get(username="root" + str(i)), - group=group, message=u"你好啊") - return HttpResponse("success") diff --git a/judge/README.md b/judge/README.md deleted file mode 100644 index c40444b1..00000000 --- a/judge/README.md +++ /dev/null @@ -1,3 +0,0 @@ -/usr/bin/docker run -t -i --privileged -v /var/test_case/:/var/judger/test_case/ -v /var/code/:/var/judger/code/ judger /bin/bash - -python judge/judger/run.py -solution_id 1 -max_cpu_time 1 -max_memory 1 -test_case_id 1 diff --git a/judge/judger_controller/settings.py b/judge/judger_controller/settings.py index 92d17807..e141c525 100644 --- a/judge/judger_controller/settings.py +++ b/judge/judger_controller/settings.py @@ -1,7 +1,12 @@ # coding=utf-8 +""" +注意: +此文件包含 celery 的部分配置,但是 celery 并不是运行在docker 中的,所以本配置文件中的 redis和 MySQL 的地址就应该是 +运行 redis 和 MySQL 的 docker 容器的地址了。怎么获取这个地址见帮助文档。测试用例的路径和源代码路径同理。 +""" # 这个redis 是 celery 使用的,包括存储队列信息还有部分统计信息 redis_config = { - "host": "121.42.32.129", + "host": "192.168.42.23", "port": 6379, "db": 0 } @@ -9,23 +14,23 @@ redis_config = { # 判题的 docker 容器的配置参数 docker_config = { - "image_name": "3da0e526934e", + "image_name": "judger", "docker_path": "docker", "shell": True } # 测试用例的路径,是主机上的实际路径 -test_case_dir = "/var/mnt/source/test_case/" +test_case_dir = "/root/test_case/" # 源代码路径,也就是 manage.py 所在的实际路径 -source_code_dir = "/var/mnt/source/OnlineJudge/" +source_code_dir = "/root/qduoj/" # 日志文件夹路径 -log_dir = "/var/log/" +log_dir = "/root/log/" # 存储提交信息的数据库,是 celery 使用的,与 oj.settings/local_settings 等区分,那是 web 服务器访问的地址 submission_db = { - "host": "127.0.0.1", + "host": "192.168.42.32", "port": 3306, "db": "oj_submission", "user": "root", diff --git a/judge/judger_controller/tasks.py b/judge/judger_controller/tasks.py index df1deaba..d120ee34 100644 --- a/judge/judger_controller/tasks.py +++ b/judge/judger_controller/tasks.py @@ -11,7 +11,8 @@ from settings import docker_config, source_code_dir, test_case_dir, log_dir, sub @app.task def judge(submission_id, time_limit, memory_limit, test_case_id): try: - command = "%s run -t -i --privileged --rm=true " \ + command = "%s run -t -i --privileged --rm " \ + "--link mysql " \ "-v %s:/var/judger/test_case/ " \ "-v %s:/var/judger/code/ " \ "-v %s:/var/judger/code/log/ " \ diff --git a/mq/scripts/info.py b/mq/scripts/info.py index 01ef6b2d..3a5fe18a 100644 --- a/mq/scripts/info.py +++ b/mq/scripts/info.py @@ -2,6 +2,9 @@ import logging import redis +import json + +from django.db import transaction from judge.judger_controller.settings import redis_config from judge.judger.result import result @@ -9,6 +12,7 @@ from submission.models import Submission from problem.models import Problem from contest.models import ContestProblem, Contest, ContestSubmission from account.models import User + logger = logging.getLogger("app_info") @@ -35,6 +39,21 @@ class MessageQueue(object): problem.save() 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 + if user.problems_status: + problems_status = json.loads(user.problems_status) + else: + problems_status = {} + problems_status[str(problem.id)] = 1 + user.problems_status = json.dumps(problems_status) + user.save() + # 普通题目的话,到这里就结束了 continue @@ -53,50 +72,46 @@ class MessageQueue(object): contest_submission = ContestSubmission.objects.get(user_id=submission.user_id, contest=contest, problem_id=contest_problem.id) # 提交次数加1 - - if submission.result == result["accepted"]: - # 避免这道题已经 ac 了,但是又重新提交了一遍 - if not contest_submission.ac: - # 这种情况是这个题目前处于错误状态,就使用已经存储了的罚时加上这道题的实际用时 - # logger.debug(contest.start_time) - # logger.debug(submission.create_time) - # logger.debug((submission.create_time - contest.start_time).total_seconds()) - # logger.debug(int((submission.create_time - contest.start_time).total_seconds() / 60)) - contest_submission.ac_time = int((submission.create_time - contest.start_time).total_seconds()) - contest_submission.total_time += contest_submission.ac_time + 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 - # 标记为已经通过 - 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() - contest_problem.save() + contest_submission.save() + contest_problem.save() except ContestSubmission.DoesNotExist: # 第一次提交 - 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) - + 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) logger.debug("Start message queue") MessageQueue().listen_task() diff --git a/problem/views.py b/problem/views.py index c66b90ed..84ca58d8 100644 --- a/problem/views.py +++ b/problem/views.py @@ -282,11 +282,15 @@ def problem_list_page(request, page=1): except Exception: pass + if request.user.is_authenticated() and request.user.problems_status: + 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, + "keyword": keyword, "tag": tag_text,"problems_status": problems_status, "tags": tags, "difficulty_order": difficulty_order}) diff --git a/static/src/css/admin.css b/static/src/css/admin.css index fb9c9f25..6a613e7c 100644 --- a/static/src/css/admin.css +++ b/static/src/css/admin.css @@ -1,4 +1,3 @@ -@import url("global.css"); @import url("bootstrap/bootstrap.min.css"); @import url("bootstrap/todc-bootstrap.min.css"); @import url("codeMirror/codemirror.css"); @@ -6,6 +5,7 @@ @import url("webuploader/webuploader.css"); @import url("datetime_picker/bootstrap-datetimepicker.css"); @import url("tagEditor/jquery.tag-editor.css"); +@import url("global.css"); #loading-gif { width: 40px; diff --git a/static/src/css/global.css b/static/src/css/global.css index 56c87eb1..2d4f6181 100644 --- a/static/src/css/global.css +++ b/static/src/css/global.css @@ -1,13 +1,17 @@ -html{ +body, button, input, select, textarea, h1, h2, h3, h4, h5, h6 { + font-family: Georgia, STHeiti, "Microsoft Yahei", SimSun, "Droid Sans"; +} + +html { height: 100%; } -body{ - height:100%; /*使内容高度和body一样*/ - margin-bottom:-80px;/*向上缩减80像素,不至于footer超出屏幕可视范围*/ +body { + height: 100%; /*使内容高度和body一样*/ + margin-bottom: -80px; /*向上缩减80像素,不至于footer超出屏幕可视范围*/ } -.main{ +.main { padding-bottom: 120px; } @@ -30,10 +34,11 @@ label { display: none } -.right{ +.right { float: right; } -.CodeMirror pre { - font-family: "Consolas","Bitstream Vera Sans Mono","Courier New", Courier, monospace !important; -} \ No newline at end of file + +pre { + font-family: "Consolas", "Bitstream Vera Sans Mono", "Courier New", Courier, monospace; +} diff --git a/static/src/css/oj.css b/static/src/css/oj.css index 86748c2d..cecc0b50 100644 --- a/static/src/css/oj.css +++ b/static/src/css/oj.css @@ -1,8 +1,7 @@ -@import url("global.css"); @import url("bootstrap/bootstrap.min.css"); @import url("bootstrap/todc-bootstrap.min.css"); @import url("codeMirror/codemirror.css"); - +@import url("global.css"); #language-selector { width: 130px; diff --git a/static/src/img/chrome.png b/static/src/img/chrome.png new file mode 100644 index 00000000..06a52bc4 Binary files /dev/null and b/static/src/img/chrome.png differ diff --git a/static/src/img/firefox.png b/static/src/img/firefox.png new file mode 100644 index 00000000..2521bcbc Binary files /dev/null and b/static/src/img/firefox.png differ diff --git a/static/src/img/ie.png b/static/src/img/ie.png new file mode 100644 index 00000000..1375a29c Binary files /dev/null and b/static/src/img/ie.png differ diff --git a/static/src/img/unsupported_browser.html b/static/src/img/unsupported_browser.html new file mode 100644 index 00000000..6d4fd6c7 --- /dev/null +++ b/static/src/img/unsupported_browser.html @@ -0,0 +1,37 @@ + + + + + 不支持的浏览器 + + + +很抱歉,我们无法完全兼容低版本的 IE 浏览器,您可以 + +
+ 使用Chrome +
+
+ +
+ 使用FireFox +
+
+ + +
+ 升级IE +
+
+ + + + \ No newline at end of file diff --git a/submission/views.py b/submission/views.py index 45e1875c..baa81d4d 100644 --- a/submission/views.py +++ b/submission/views.py @@ -46,7 +46,14 @@ class SubmissionAPIView(APIView): judge.delay(submission.id, problem.time_limit, problem.memory_limit, problem.test_case_id) except Exception: return error_response(u"提交判题任务失败") - + # 修改用户解题状态 + if request.user.problems_status: + problems_status = json.loads(request.user.problems_status) + else: + problems_status = {} + problems_status[str(data["problem_id"])] = 2 + request.user.problems_status = json.dumps(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") diff --git a/template/src/admin/admin.html b/template/src/admin/admin.html index 459a69fc..0e470c6e 100644 --- a/template/src/admin/admin.html +++ b/template/src/admin/admin.html @@ -54,13 +54,11 @@ - - - +
diff --git a/template/src/oj/contest/contest_rank.html b/template/src/oj/contest/contest_rank.html index aefb1990..20b2d6ab 100644 --- a/template/src/oj/contest/contest_rank.html +++ b/template/src/oj/contest/contest_rank.html @@ -45,7 +45,12 @@ {% for item in result %} {{ forloop.counter }} - {{ item.username }} + + {{ item.username }} + {% if show_real_name %} + ({{ item.real_name }}) + {% endif %} + {{ item.total_ac }} / {{ item.total_submit }} {% if item.total_time %}{{ item.total_time }}{% else %}--{% endif %} {% for problem in item.problems %} diff --git a/template/src/oj/problem/problem.html b/template/src/oj/problem/problem.html index 2ea79140..2fc9b95a 100644 --- a/template/src/oj/problem/problem.html +++ b/template/src/oj/problem/problem.html @@ -59,6 +59,12 @@ {% endfor %}

+ {% if problem.source %} +
+ +
{{ problem.source }}
+
+ {% endif %}
diff --git a/template/src/oj/problem/problem_list.html b/template/src/oj/problem/problem_list.html index 7a9b1dce..1a1cd139 100644 --- a/template/src/oj/problem/problem_list.html +++ b/template/src/oj/problem/problem_list.html @@ -28,7 +28,7 @@ {% for item in problems %} - + {{ item.id }} {{ item.title }} diff --git a/template/src/oj_base.html b/template/src/oj_base.html index f90052cc..1910ff0e 100644 --- a/template/src/oj_base.html +++ b/template/src/oj_base.html @@ -1,5 +1,5 @@ - + @@ -21,13 +21,11 @@ - - - +
diff --git a/tools/release_static.py b/tools/release_static.py index 0a51b8a7..2ab87e5d 100644 --- a/tools/release_static.py +++ b/tools/release_static.py @@ -9,6 +9,12 @@ template_release_path = "template/release/" static_src_path = "static/src/" static_release_path = "static/release/" + +print "Begin to compress js" +if os.system("node static/src/js/r.js -o static/src/js/build.js"): + print "Failed to compress js, exit" + exit() + try: # 删除模板的 release 文件夹 shutil.rmtree(template_release_path) @@ -30,7 +36,6 @@ name_map = {} def process(match): file_path = match.group(1).replace("/static/", "") - # print file_path, match.group(), match.group(1) if not os.path.exists(static_release_path + file_path): return match.group(0) @@ -49,9 +54,11 @@ def process(match): return match.group(0) +print "Begin to add md5 stamp in html" for root, dirs, files in os.walk(template_release_path): for name in files: html_path = os.path.join(root, name) + print "Processing: " + html_path html_content = open(html_path, "r").read() js_replaced_html_content = re.sub(js_re, process, html_content) css_replaced_html_content = re.sub(css_re, process, js_replaced_html_content) @@ -59,3 +66,5 @@ for root, dirs, files in os.walk(template_release_path): f = open(html_path, "w") f.write(css_replaced_html_content) f.close() + +print "Done" diff --git a/utils/templatetags/problem.py b/utils/templatetags/problem.py index 412c91db..31d290df 100644 --- a/utils/templatetags/problem.py +++ b/utils/templatetags/problem.py @@ -8,7 +8,16 @@ def get_problem_accepted_radio(problem): return "0%" +def get_problem_status(problems_status, problem_id): + + if str(problem_id) in problems_status: + if problems_status[str(problem_id)] == 1: + return "glyphicon glyphicon-ok ac-flag" + return "glyphicon glyphicon-minus dealing-flag" + return "" + from django import template register = template.Library() register.filter("accepted_radio", get_problem_accepted_radio) +register.simple_tag(get_problem_status, name="get_problem_status")