diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..b6cb82c --- /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/.gitignore b/.gitignore index 285cfd8..e087361 100644 --- a/.gitignore +++ b/.gitignore @@ -58,6 +58,9 @@ log/ static/release/css static/release/js static/release/img +static/src/upload_image/* build.txt tmp/ -test_case/ \ No newline at end of file +test_case/ +release/ +upload/ \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..7126efb --- /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 e69de29..cdf478d 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 0000000..58c8a31 --- /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/migrations/0003_auto_20150915_2025.py b/account/migrations/0003_auto_20150915_2025.py new file mode 100644 index 0000000..ff6b0ea --- /dev/null +++ b/account/migrations/0003_auto_20150915_2025.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', '0002_user_problems_status'), + ] + + operations = [ + migrations.AlterField( + model_name='user', + name='problems_status', + field=models.TextField(default=b'{}'), + ), + ] diff --git a/account/migrations/0004_remove_user_problems_status.py b/account/migrations/0004_remove_user_problems_status.py new file mode 100644 index 0000000..64f8ae0 --- /dev/null +++ b/account/migrations/0004_remove_user_problems_status.py @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('account', '0003_auto_20150915_2025'), + ] + + operations = [ + migrations.RemoveField( + model_name='user', + name='problems_status', + ), + ] diff --git a/account/migrations/0005_user_problems_status.py b/account/migrations/0005_user_problems_status.py new file mode 100644 index 0000000..bfb1db0 --- /dev/null +++ b/account/migrations/0005_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', '0004_remove_user_problems_status'), + ] + + operations = [ + migrations.AddField( + model_name='user', + name='problems_status', + field=models.TextField(default=b'{}'), + ), + ] diff --git a/account/models.py b/account/models.py index df25329..3245333 100644 --- a/account/models.py +++ b/account/models.py @@ -30,6 +30,8 @@ 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(default="{}") USERNAME_FIELD = 'username' REQUIRED_FIELDS = [] diff --git a/account/serializers.py b/account/serializers.py index 179d28d..c2852a3 100644 --- a/account/serializers.py +++ b/account/serializers.py @@ -7,6 +7,7 @@ from .models import User class UserLoginSerializer(serializers.Serializer): username = serializers.CharField(max_length=30) password = serializers.CharField(max_length=30) + captcha = serializers.CharField(required=False, min_length=4, max_length=4) class UsernameCheckSerializer(serializers.Serializer): @@ -22,11 +23,13 @@ class UserRegisterSerializer(serializers.Serializer): real_name = serializers.CharField(max_length=30) password = serializers.CharField(max_length=30, min_length=6) email = serializers.EmailField(max_length=254) + captcha = serializers.CharField(max_length=4, min_length=4) class UserChangePasswordSerializer(serializers.Serializer): old_password = serializers.CharField() new_password = serializers.CharField(max_length=30, min_length=6) + captcha = serializers.CharField(max_length=4, min_length=4) class UserSerializer(serializers.ModelSerializer): diff --git a/account/views.py b/account/views.py index 1da8967..2583f36 100644 --- a/account/views.py +++ b/account/views.py @@ -5,14 +5,15 @@ from django.shortcuts import render from django.db.models import Q 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.captcha import Captcha from .decorators import login_required from .models import User from .serializers import (UserLoginSerializer, UsernameCheckSerializer, - UserRegisterSerializer, UserChangePasswordSerializer, - EmailCheckSerializer, UserSerializer, EditUserSerializer) + UserRegisterSerializer, UserChangePasswordSerializer, + EmailCheckSerializer, UserSerializer, EditUserSerializer) class UserLoginAPIView(APIView): @@ -28,6 +29,12 @@ class UserLoginAPIView(APIView): user = auth.authenticate(username=data["username"], password=data["password"]) # 用户名或密码错误的话 返回None if user: + if user.admin_type > 0: + if "captcha" not in data: + return error_response(u"请填写验证码!") + captcha = Captcha(request) + if not captcha.check(data["captcha"]): + return error_response(u"验证码错误") auth.login(request, user) return success_response(u"登录成功") else: @@ -35,11 +42,24 @@ class UserLoginAPIView(APIView): else: return serializer_invalid_response(serializer) + @login_required def logout(request): auth.logout(request) return http.HttpResponseRedirect("/") + +def index_page(request): + if not request.user.is_authenticated(): + return render(request, "oj/index.html") + + try: + if request.META['HTTP_REFERER']: + return render(request, "oj/index.html") + except KeyError: + return http.HttpResponseRedirect('/problems/') + + class UserRegisterAPIView(APIView): def post(self, request): """ @@ -50,6 +70,9 @@ class UserRegisterAPIView(APIView): serializer = UserRegisterSerializer(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.objects.get(username=data["username"]) return error_response(u"用户名已存在") @@ -79,6 +102,9 @@ class UserChangePasswordAPIView(APIView): serializer = UserChangePasswordSerializer(data=request.data) if serializer.is_valid(): data = serializer.data + captcha = Captcha(request) + if not captcha.check(data["captcha"]): + return error_response(u"验证码错误") username = request.user.username user = auth.authenticate(username=username, password=data["old_password"]) if user: @@ -92,39 +118,35 @@ class UserChangePasswordAPIView(APIView): class UsernameCheckAPIView(APIView): - def post(self, request): + def get(self, request): """ - 检测用户名是否存在,存在返回True,不存在返回False + 检测用户名是否存在,存在返回状态码400,不存在返回200 --- - request_serializer: UsernameCheckSerializer """ - serializer = UsernameCheckSerializer(data=request.data) - if serializer.is_valid(): + username = request.GET.get("username", None) + if username: try: - User.objects.get(username=serializer.data["username"]) - return success_response(True) + User.objects.get(username=username) + return Response(status=400) except User.DoesNotExist: - return success_response(False) - else: - return serializer_invalid_response(serializer) + return Response(status=200) + return Response(status=200) class EmailCheckAPIView(APIView): - def post(self, request): + def get(self, request): """ - 检测邮箱是否存在,存在返回True,不存在返回False + 检测邮箱是否存在,存在返回状态码400,不存在返回200 --- - request_serializer: EmailCheckSerializer """ - serializer = EmailCheckSerializer(data=request.data) - if serializer.is_valid(): + email = request.GET.get("email", None) + if email: try: - User.objects.get(email=serializer.data["email"]) - return success_response(True) + User.objects.get(email=email) + return Response(status=400) except User.DoesNotExist: - return success_response(False) - else: - return serializer_invalid_response(serializer) + return Response(status=200) + return Response(status=200) class UserAdminAPIView(APIView): @@ -189,3 +211,19 @@ class UserInfoAPIView(APIView): response_serializer: UserSerializer """ return success_response(UserSerializer(request.user).data) + + +class AccountSecurityAPIView(APIView): + def get(self, request): + """ + 判断用户登录是否需要验证码 + --- + """ + username = request.GET.get("username", None) + if username: + try: + User.objects.get(username=username, admin_type__gt=0) + except User.DoesNotExist: + return success_response({"applied_captcha": False}) + return success_response({"applied_captcha": True}) + return success_response({"applied_captcha": False}) diff --git a/announcement/views.py b/announcement/views.py index 818aa0a..6790e8c 100644 --- a/announcement/views.py +++ b/announcement/views.py @@ -16,8 +16,21 @@ def announcement_page(request, announcement_id): try: announcement = Announcement.objects.get(id=announcement_id, visible=True) except Announcement.DoesNotExist: - return error_page(request, u"模板不存在") - return render(request, "oj/announcement/announcement.html", {"announcement": announcement}) + return error_page(request, u"公告不存在") + # 公开的公告 + if announcement.is_global == 0: + return render(request, "oj/announcement/announcement.html", {"announcement": announcement}) + else: + if not request.user.is_authenticated(): + return error_page(request, u"公告不存在") + # 判断是不是在组里面 + if request.user.admin_type == SUPER_ADMIN or request.user == announcement.created_by: + return render(request, "oj/announcement/announcement.html", {"announcement": announcement}) + else: + if request.user.groups.filter(id__in=[item.id for item in announcement.groups.all()]).exists(): + return render(request, "oj/announcement/announcement.html", {"announcement": announcement}) + else: + return error_page(request, u"公告不存在") class AnnouncementAdminAPIView(APIView): diff --git a/contest/migrations/0007_contestsubmission_ac_time.py b/contest/migrations/0007_contestsubmission_ac_time.py new file mode 100644 index 0000000..d7bc904 --- /dev/null +++ b/contest/migrations/0007_contestsubmission_ac_time.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('contest', '0006_merge'), + ] + + operations = [ + migrations.AddField( + model_name='contestsubmission', + name='ac_time', + field=models.IntegerField(default=0), + ), + ] diff --git a/contest/migrations/0008_auto_20150912_1912.py b/contest/migrations/0008_auto_20150912_1912.py new file mode 100644 index 0000000..ee2a8f5 --- /dev/null +++ b/contest/migrations/0008_auto_20150912_1912.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('contest', '0007_contestsubmission_ac_time'), + ] + + operations = [ + migrations.RenameField( + model_name='contest', + old_name='show_rank', + new_name='real_time_rank', + ), + ] diff --git a/contest/migrations/0009_contestsubmission_first_achieved.py b/contest/migrations/0009_contestsubmission_first_achieved.py new file mode 100644 index 0000000..ce9529c --- /dev/null +++ b/contest/migrations/0009_contestsubmission_first_achieved.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('contest', '0008_auto_20150912_1912'), + ] + + operations = [ + migrations.AddField( + model_name='contestsubmission', + name='first_achieved', + field=models.BooleanField(default=False), + ), + ] diff --git a/contest/models.py b/contest/models.py index 338e3b1..7331ec2 100644 --- a/contest/models.py +++ b/contest/models.py @@ -6,20 +6,25 @@ from account.models import User from problem.models import AbstractProblem from group.models import Group +GROUP_CONTEST = 0 +PUBLIC_CONTEST = 1 +PASSWORD_PROTECTED_CONTEST = 2 + class Contest(models.Model): title = models.CharField(max_length=40, unique=True) description = models.TextField() # 比赛模式:0 即为是acm模式,1 即为是按照总的 ac 题目数量排名模式 mode = models.IntegerField() - # 是否显示排名结果 - show_rank = models.BooleanField() + # 是否显示实时排名结果 + real_time_rank = models.BooleanField() # 是否显示别人的提交记录 show_user_submission = models.BooleanField() # 只能超级管理员创建公开赛,管理员只能创建小组内部的比赛 # 如果这一项不为空,即为有密码的公开赛,没有密码的可以为小组赛或者是公开赛(此时用比赛的类型来表示) password = models.CharField(max_length=30, blank=True, null=True) - # 比赛的类型: 0 即为是小组赛,1 即为是无密码的公开赛,2 即为是有密码的公开赛 + # 比赛的类型: 0 即为是小组赛(GROUP_CONTEST),1 即为是无密码的公开赛(PUBLIC_CONTEST), + # 2 即为是有密码的公开赛(PASSWORD_PUBLIC_CONTEST) contest_type = models.IntegerField() # 开始时间 start_time = models.DateTimeField() @@ -84,8 +89,12 @@ class ContestSubmission(models.Model): total_submission_number = models.IntegerField(default=1) # 这道题是 AC 还是没过 ac = models.BooleanField() + # ac 用时以秒计 + ac_time = models.IntegerField(default=0) # 总的时间,用于acm 类型的,也需要保存罚时 total_time = models.IntegerField(default=0) + # 第一个解出此题目 + first_achieved = models.BooleanField(default=False) class Meta: db_table = "contest_submission" diff --git a/contest/serializers.py b/contest/serializers.py index 65b2fe7..8f3bf0b 100644 --- a/contest/serializers.py +++ b/contest/serializers.py @@ -13,7 +13,7 @@ class CreateContestSerializer(serializers.Serializer): description = serializers.CharField(max_length=5000) mode = serializers.IntegerField() contest_type = serializers.IntegerField() - show_rank = serializers.BooleanField() + real_time_rank = serializers.BooleanField() show_user_submission = serializers.BooleanField() password = serializers.CharField(max_length=30, required=False, default=None) start_time = serializers.DateTimeField() @@ -47,7 +47,7 @@ class EditContestSerializer(serializers.Serializer): description = serializers.CharField(max_length=10000) mode = serializers.IntegerField() contest_type = serializers.IntegerField() - show_rank = serializers.BooleanField() + real_time_rank = serializers.BooleanField() show_user_submission = serializers.BooleanField() password = serializers.CharField(max_length=30, required=False, default=None) start_time = serializers.DateTimeField() diff --git a/contest/tests.py b/contest/tests.py index 626a3ab..8875997 100644 --- a/contest/tests.py +++ b/contest/tests.py @@ -1,18 +1,16 @@ # coding=utf-8 import json + from django.core.urlresolvers import reverse from django.test import TestCase, Client -from django.http import HttpResponse - from rest_framework.test import APITestCase, APIClient from account.models import User from group.models import Group from contest.models import Contest, ContestProblem from .models import ContestSubmission -from announcement.models import Announcement +from .models import GROUP_CONTEST, PASSWORD_PROTECTED_CONTEST from account.models import REGULAR_USER, ADMIN, SUPER_ADMIN -from decorators import check_user_contest_permission class ContestAdminAPITest(APITestCase): @@ -32,15 +30,17 @@ class ContestAdminAPITest(APITestCase): join_group_setting=0, visible=True, admin=user2) self.group2 = Group.objects.create(name="group2", description="des0", - join_group_setting=0, visible=True, - admin=user1) + join_group_setting=0, visible=True, + admin=user1) self.global_contest = Contest.objects.create(title="titlex", description="descriptionx", mode=1, - contest_type=2, show_rank=True, show_user_submission=True, + contest_type=PASSWORD_PROTECTED_CONTEST, show_rank=True, + show_user_submission=True, start_time="2015-08-15T10:00:00.000Z", end_time="2015-08-15T12:00:00.000Z", password="aacc", created_by=User.objects.get(username="test1")) self.group_contest = Contest.objects.create(title="titley", description="descriptiony", mode=1, - contest_type=2, show_rank=True, show_user_submission=True, + contest_type=PASSWORD_PROTECTED_CONTEST, show_rank=True, + show_user_submission=True, start_time="2015-08-15T10:00:00.000Z", end_time="2015-08-15T12:00:00.000Z", password="aacc", created_by=User.objects.get(username="test1")) @@ -54,7 +54,7 @@ class ContestAdminAPITest(APITestCase): def test_global_contest_does_not_has_privileges(self): self.client.login(username="test2", password="testbb") - data = {"title": "title0", "description": "description0", "mode": 1, "contest_type": 2, + data = {"title": "title0", "description": "description0", "mode": 1, "contest_type": PASSWORD_PROTECTED_CONTEST, "show_rank": True, "show_user_submission": True, "start_time": "2015-08-15T10:00:00.000Z", "end_time": "2015-08-15T12:00:00.000Z", "password": "aabb", "visible": True} response = self.client.post(self.url, data=data) @@ -62,7 +62,7 @@ class ContestAdminAPITest(APITestCase): def test_global_contest_password_exists(self): self.client.login(username="test1", password="testaa") - data = {"title": "title0", "description": "description0", "mode": 1, "contest_type": 2, + data = {"title": "title0", "description": "description0", "mode": 1, "contest_type": PASSWORD_PROTECTED_CONTEST, "show_rank": True, "show_user_submission": True, "start_time": "2015-08-15T10:00:00.000Z", "end_time": "2015-08-15T12:00:00.000Z", "visible": True} response = self.client.post(self.url, data=data) @@ -70,7 +70,7 @@ class ContestAdminAPITest(APITestCase): def test_group_contest_group_at_least_one(self): self.client.login(username="test1", password="testaa") - data = {"title": "title0", "description": "description0", "mode": 1, "contest_type": 0, + data = {"title": "title0", "description": "description0", "mode": 1, "contest_type": GROUP_CONTEST, "show_rank": True, "show_user_submission": True, "start_time": "2015-08-15T10:00:00.000Z", "end_time": "2015-08-15T12:00:00.000Z", "visible": True} response = self.client.post(self.url, data=data) @@ -78,7 +78,7 @@ class ContestAdminAPITest(APITestCase): def test_global_contest_successfully(self): self.client.login(username="test1", password="testaa") - data = {"title": "title1", "description": "description1", "mode": 1, "contest_type": 2, + data = {"title": "title1", "description": "description1", "mode": 1, "contest_type": PASSWORD_PROTECTED_CONTEST, "show_rank": True, "show_user_submission": True, "start_time": "2015-08-15T10:00:00.000Z", "end_time": "2015-08-15T12:00:00.000Z", "password": "aabb", "visible": True} response = self.client.post(self.url, data=data) @@ -86,7 +86,7 @@ class ContestAdminAPITest(APITestCase): def test_group_contest_super_admin_successfully(self): self.client.login(username="test1", password="testaa") - data = {"title": "title3", "description": "description3", "mode": 1, "contest_type": 0, + data = {"title": "title3", "description": "description3", "mode": 1, "contest_type": GROUP_CONTEST, "show_rank": True, "show_user_submission": True, "start_time": "2015-08-15T10:00:00.000Z", "end_time": "2015-08-15T12:00:00.000Z", "groups": [self.group.id], "visible": True} response = self.client.post(self.url, data=data) @@ -94,7 +94,7 @@ class ContestAdminAPITest(APITestCase): def test_group_contest_admin_successfully(self): self.client.login(username="test2", password="testbb") - data = {"title": "title6", "description": "description6", "mode": 2, "contest_type": 0, + data = {"title": "title6", "description": "description6", "mode": 2, "contest_type": GROUP_CONTEST, "show_rank": True, "show_user_submission": True, "start_time": "2015-08-15T10:00:00.000Z", "end_time": "2015-08-15T12:00:00.000Z", "groups": [self.group.id], "visible": True} response = self.client.post(self.url, data=data) @@ -102,7 +102,7 @@ class ContestAdminAPITest(APITestCase): def test_time_error(self): self.client.login(username="test1", password="testaa") - data = {"title": "title2", "description": "description2", "mode": 1, "contest_type": 2, + data = {"title": "title2", "description": "description2", "mode": 1, "contest_type": PASSWORD_PROTECTED_CONTEST, "show_rank": True, "show_user_submission": True, "start_time": "2015-08-15T12:00:00.000Z", "end_time": "2015-08-15T10:00:00.000Z", "password": "aabb", "visible": True} response = self.client.post(self.url, data=data) @@ -110,7 +110,7 @@ class ContestAdminAPITest(APITestCase): def test_contest_has_exists(self): self.client.login(username="test1", password="testaa") - data = {"title": "titlex", "description": "descriptionx", "mode": 1, "contest_type": 2, + data = {"title": "titlex", "description": "descriptionx", "mode": 1, "contest_type": PASSWORD_PROTECTED_CONTEST, "show_rank": True, "show_user_submission": True, "start_time": "2015-08-15T10:00:00.000Z", "end_time": "2015-08-15T12:00:00.000Z", "password": "aabb", "visible": True} response = self.client.post(self.url, data=data) @@ -126,7 +126,7 @@ class ContestAdminAPITest(APITestCase): def test_contest_does_not_exist(self): self.client.login(username="test1", password="testaa") data = {"id": self.global_contest.id + 10, "title": "title2", "description": "description2", "mode": 1, - "contest_type": 2, "show_rank": True, "show_user_submission": True, + "contest_type": PASSWORD_PROTECTED_CONTEST, "show_rank": True, "show_user_submission": True, "start_time": "2015-08-15T10:00:00.000Z", "end_time": "2015-08-15T12:00:00.000Z", "password": "aabb", "visible": True} response = self.client.put(self.url, data=data) @@ -135,7 +135,7 @@ class ContestAdminAPITest(APITestCase): def test_edit_global_contest_successfully(self): self.client.login(username="test1", password="testaa") data = {"id": self.global_contest.id, "title": "titlez", "description": "descriptionz", "mode": 1, - "contest_type": 2, "show_rank": True, "show_user_submission": True, + "contest_type": PASSWORD_PROTECTED_CONTEST, "show_rank": True, "show_user_submission": True, "start_time": "2015-08-15T10:00:00.000Z", "end_time": "2015-08-15T13:00:00.000Z", "password": "aabb", "visible": True} response = self.client.put(self.url, data=data) @@ -146,7 +146,7 @@ class ContestAdminAPITest(APITestCase): def test_edit_group_contest_successfully(self): self.client.login(username="test1", password="testaa") data = {"id": self.group_contest.id, "title": "titleyyy", "description": "descriptionyyyy", "mode": 1, - "contest_type": 0, "show_rank": True, "show_user_submission": True, + "contest_type": GROUP_CONTEST, "show_rank": True, "show_user_submission": True, "start_time": "2015-08-15T10:00:00.000Z", "end_time": "2015-08-15T13:00:00.000Z", "groups": [self.group.id], "visible": False} response = self.client.put(self.url, data=data) @@ -158,7 +158,7 @@ class ContestAdminAPITest(APITestCase): def test_edit_group_contest_unsuccessfully(self): self.client.login(username="test2", password="testbb") data = {"id": self.group_contest.id, "title": "titleyyy", "description": "descriptionyyyy", "mode": 1, - "contest_type": 0, "show_rank": True, "show_user_submission": True, + "contest_type": GROUP_CONTEST, "show_rank": True, "show_user_submission": True, "start_time": "2015-08-15T10:00:00.000Z", "end_time": "2015-08-15T13:00:00.000Z", "groups": [self.group.id], "visible": False} response = self.client.put(self.url, data=data) @@ -167,7 +167,7 @@ class ContestAdminAPITest(APITestCase): def test_edit_group_at_least_one(self): self.client.login(username="test1", password="testaa") data = {"id": self.group_contest.id, "title": "titleyyy", "description": "descriptionyyyy", "mode": 1, - "contest_type": 0, "show_rank": True, "show_user_submission": True, + "contest_type": GROUP_CONTEST, "show_rank": True, "show_user_submission": True, "start_time": "2015-08-15T10:00:00.000Z", "end_time": "2015-08-15T13:00:00.000Z", "visible": True} response = self.client.put(self.url, data=data) self.assertEqual(response.data, {"code": 1, "data": u"请至少选择一个小组"}) @@ -175,7 +175,7 @@ class ContestAdminAPITest(APITestCase): def test_edit_contest_has_exists(self): self.client.login(username="test1", password="testaa") data = {"id": self.global_contest.id, "title": "titley", "description": "descriptiony", "mode": 1, - "contest_type": 2, "show_rank": True, "show_user_submission": True, + "contest_type": PASSWORD_PROTECTED_CONTEST, "show_rank": True, "show_user_submission": True, "start_time": "2015-08-15T10:00:00.000Z", "end_time": "2015-08-15T12:00:00.000Z", "password": "aabb", "visible": True} response = self.client.put(self.url, data=data) @@ -184,7 +184,7 @@ class ContestAdminAPITest(APITestCase): def test_edit_global_contest_does_not_has_privileges(self): self.client.login(username="test2", password="testbb") data = {"id": self.global_contest.id, "title": "titlexxxxxxxxx", "description": "descriptionxxxxxx", "mode": 1, - "contest_type": 2, "show_rank": True, "show_user_submission": True, + "contest_type": PASSWORD_PROTECTED_CONTEST, "show_rank": True, "show_user_submission": True, "start_time": "2015-08-15T10:00:00.000Z", "end_time": "2015-08-15T12:00:00.000Z", "password": "aabb", "visible": True} response = self.client.put(self.url, data=data) @@ -193,8 +193,7 @@ class ContestAdminAPITest(APITestCase): def test_edit_global_contest_password_exists(self): self.client.login(username="test1", password="testaa") data = {"id": self.global_contest.id, "title": "title0", "description": "description0", "mode": 1, - "contest_type": 2, - "show_rank": True, "show_user_submission": True, "start_time": "2015-08-15T10:00:00.000Z", + "contest_type": PASSWORD_PROTECTED_CONTEST, "show_rank": True, "show_user_submission": True, "start_time": "2015-08-15T10:00:00.000Z", "end_time": "2015-08-15T12:00:00.000Z", "visible": True} response = self.client.put(self.url, data=data) self.assertEqual(response.data, {"code": 1, "data": u"此比赛为有密码的公开赛,密码不可为空"}) @@ -202,7 +201,7 @@ class ContestAdminAPITest(APITestCase): def test_edit_time_error(self): self.client.login(username="test1", password="testaa") data = {"id": self.global_contest.id, "title": "titleaaaa", "description": "descriptionaaaaa", "mode": 1, - "contest_type": 2, "show_rank": True, "show_user_submission": True, + "contest_type": PASSWORD_PROTECTED_CONTEST, "show_rank": True, "show_user_submission": True, "start_time": "2015-08-15T12:00:00.000Z", "end_time": "2015-08-15T10:00:00.000Z", "password": "aabb", "visible": True} response = self.client.put(self.url, data=data) @@ -245,7 +244,8 @@ class ContestProblemAdminAPItEST(APITestCase): self.user3.save() self.client.login(username="test1", password="testaa") self.global_contest = Contest.objects.create(title="titlex", description="descriptionx", mode=1, - contest_type=2, show_rank=True, show_user_submission=True, + contest_type=PASSWORD_PROTECTED_CONTEST, show_rank=True, + show_user_submission=True, start_time="2015-08-15T10:00:00.000Z", end_time="2015-08-15T12:00:00.000Z", password="aacc", created_by=User.objects.get(username="test1")) @@ -373,7 +373,7 @@ class ContestProblemAdminAPItEST(APITestCase): def test_query_contest_problem_exists_by_contest_id(self): self.client.login(username="test3", password="testaa") - response = self.client.get(self.url + "?contest_id="+ str(self.global_contest.id)) + response = self.client.get(self.url + "?contest_id=" + str(self.global_contest.id)) self.assertEqual(response.data["code"], 0) self.assertEqual(len(response.data["data"]), 0) @@ -414,7 +414,8 @@ class ContestPasswordVerifyAPITest(APITestCase): self.user2.save() self.client.login(username="test1", password="testaa") self.global_contest = Contest.objects.create(title="titlex", description="descriptionx", mode=1, - contest_type=2, show_rank=True, show_user_submission=True, + contest_type=PASSWORD_PROTECTED_CONTEST, show_rank=True, + show_user_submission=True, start_time="2015-08-15T10:00:00.000Z", end_time="2015-08-15T12:00:00.000Z", password="aacc", created_by=User.objects.get(username="test1")) @@ -453,7 +454,8 @@ class ContestPageTest(TestCase): self.user1.save() self.client.login(username="test1", password="testaa") self.global_contest = Contest.objects.create(title="titlex", description="descriptionx", mode=1, - contest_type=2, show_rank=True, show_user_submission=True, + contest_type=PASSWORD_PROTECTED_CONTEST, show_rank=True, + show_user_submission=True, start_time="2015-08-15T10:00:00.000Z", end_time="2015-08-15T12:00:00.000Z", password="aacc", created_by=User.objects.get(username="test1")) @@ -476,7 +478,8 @@ class ContestProblemPageTest(TestCase): self.user1.save() self.client.login(username="test1", password="testaa") self.global_contest = Contest.objects.create(title="titlex", description="descriptionx", mode=1, - contest_type=2, show_rank=True, show_user_submission=True, + contest_type=PASSWORD_PROTECTED_CONTEST, show_rank=True, + show_user_submission=True, start_time="2015-08-15T10:00:00.000Z", end_time="2015-08-15T12:00:00.000Z", password="aacc", created_by=User.objects.get(username="test1")) @@ -519,7 +522,8 @@ class ContestProblemListPageTest(TestCase): self.user1.save() self.client.login(username="test1", password="testaa") self.global_contest = Contest.objects.create(title="titlex", description="descriptionx", mode=1, - contest_type=2, show_rank=True, show_user_submission=True, + contest_type=PASSWORD_PROTECTED_CONTEST, show_rank=True, + show_user_submission=True, start_time="2015-08-15T10:00:00.000Z", end_time="2015-08-15T12:00:00.000Z", password="aacc", created_by=User.objects.get(username="test1")) @@ -555,7 +559,8 @@ class ContestListPageTest(TestCase): self.url = reverse('contest_list_page') self.client.login(username="test1", password="testaa") self.global_contest = Contest.objects.create(title="titlex", description="descriptionx", mode=1, - contest_type=2, show_rank=True, show_user_submission=True, + contest_type=PASSWORD_PROTECTED_CONTEST, show_rank=True, + show_user_submission=True, start_time="2015-08-15T10:00:00.000Z", end_time="2015-08-15T12:00:00.000Z", password="aacc", created_by=User.objects.get(username="test1")) diff --git a/contest/views.py b/contest/views.py index f945b41..b38161a 100644 --- a/contest/views.py +++ b/contest/views.py @@ -1,28 +1,30 @@ # coding=utf-8 import json import datetime -from functools import wraps -from django.utils.timezone import now + from django.shortcuts import render from django.db import IntegrityError from django.utils import dateparse -from django.db.models import Q, Count, Sum +from django.db.models import Q, Sum from django.core.paginator import Paginator -from rest_framework.views import APIView -from utils.shortcuts import (serializer_invalid_response, error_response, - success_response, paginate, rand_str, error_page) +from django.utils.timezone import now -from account.models import REGULAR_USER, ADMIN, SUPER_ADMIN, User +from rest_framework.views import APIView + +from utils.shortcuts import (serializer_invalid_response, error_response, + success_response, paginate, error_page) +from account.models import SUPER_ADMIN, User from account.decorators import login_required from group.models import Group -from announcement.models import Announcement - from .models import Contest, ContestProblem, ContestSubmission +from .models import GROUP_CONTEST, PUBLIC_CONTEST, PASSWORD_PROTECTED_CONTEST from .decorators import check_user_contest_permission from .serializers import (CreateContestSerializer, ContestSerializer, EditContestSerializer, CreateContestProblemSerializer, ContestProblemSerializer, - EditContestProblemSerializer, ContestPasswordVerifySerializer, + ContestPasswordVerifySerializer, EditContestProblemSerializer) +from oj.settings import REDIS_CACHE +import redis class ContestAdminAPIView(APIView): @@ -37,17 +39,18 @@ class ContestAdminAPIView(APIView): if serializer.is_valid(): data = serializer.data groups = [] - # 首先判断比赛的类型: 0 即为是小组赛,1 即为是无密码的公开赛,2 即为是有密码的公开赛 + # 首先判断比赛的类型: 0 即为是小组赛(GROUP_CONTEST),1 即为是无密码的公开赛(PUBLIC_CONTEST), + # 2 即为是有密码的公开赛(PASSWORD_PUBLIC_CONTEST) # 此时为有密码的公开赛,并且此时只能超级管理员才有权限此创建比赛 - if data["contest_type"] in [1, 2]: + if data["contest_type"] in [PUBLIC_CONTEST, PASSWORD_PROTECTED_CONTEST]: if request.user.admin_type != SUPER_ADMIN: return error_response(u"只有超级管理员才可创建公开赛") - if data["contest_type"] == 2: + if data["contest_type"] == PASSWORD_PROTECTED_CONTEST: if not data["password"]: return error_response(u"此比赛为有密码的公开赛,密码不可为空") # 没有密码的公开赛 没有密码的小组赛 - elif data["contest_type"] == 0: + elif data["contest_type"] == GROUP_CONTEST: if request.user.admin_type == SUPER_ADMIN: groups = Group.objects.filter(id__in=data["groups"]) else: @@ -59,7 +62,7 @@ class ContestAdminAPIView(APIView): try: contest = Contest.objects.create(title=data["title"], description=data["description"], mode=data["mode"], contest_type=data["contest_type"], - show_rank=data["show_rank"], password=data["password"], + real_time_rank=data["real_time_rank"], password=data["password"], show_user_submission=data["show_user_submission"], start_time=dateparse.parse_datetime(data["start_time"]), end_time=dateparse.parse_datetime(data["end_time"]), @@ -92,13 +95,13 @@ class ContestAdminAPIView(APIView): return error_response(u"该比赛名称已经存在") except Contest.DoesNotExist: pass - if data["contest_type"] in [1, 2]: + if data["contest_type"] in [PUBLIC_CONTEST, PASSWORD_PROTECTED_CONTEST]: if request.user.admin_type != SUPER_ADMIN: return error_response(u"只有超级管理员才可创建公开赛") - if data["contest_type"] == 2: + if data["contest_type"] == PASSWORD_PROTECTED_CONTEST: if not data["password"]: return error_response(u"此比赛为有密码的公开赛,密码不可为空") - elif data["contest_type"] == 0: + elif data["contest_type"] == GROUP_CONTEST: if request.user.admin_type == SUPER_ADMIN: groups = Group.objects.filter(id__in=data["groups"]) else: @@ -113,7 +116,7 @@ class ContestAdminAPIView(APIView): contest.description = data["description"] contest.mode = data["mode"] contest.contest_type = data["contest_type"] - contest.show_rank = data["show_rank"] + contest.real_time_rank = data["real_time_rank"] contest.show_user_submission = data["show_user_submission"] contest.start_time = dateparse.parse_datetime(data["start_time"]) contest.end_time = dateparse.parse_datetime(data["end_time"]) @@ -256,7 +259,7 @@ class ContestPasswordVerifyAPIView(APIView): if serializer.is_valid(): data = request.data try: - contest = Contest.objects.get(id=data["contest_id"], contest_type=2) + contest = Contest.objects.get(id=data["contest_id"], contest_type=PASSWORD_PROTECTED_CONTEST) except Contest.DoesNotExist: return error_response(u"比赛不存在") @@ -317,13 +320,23 @@ def contest_problems_list_page(request, contest_id): 比赛所有题目的列表页 """ try: - contest_problems = ContestProblem.objects.filter(contest=Contest.objects.get(id=contest_id)).order_by("sort_index") - except ContestProblem.DoesNotExist: - return error_page(request, u"比赛题目不存在") - # 右侧的公告列表 - announcements = Announcement.objects.filter(is_global=True, visible=True).order_by("-create_time") + contest = Contest.objects.get(id=contest_id) + except Contest.DoesNotExist: + return error_page(request, u"比赛不存在") + 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, - "announcements": announcements, "contest": {"id": contest_id}}) @@ -341,10 +354,9 @@ def contest_list_page(request, page=1): # 筛选我能参加的比赛 join = request.GET.get("join", None) - if join: - contests = contests.filter(Q(contest_type__in=[1, 2]) | Q(groups__in=request.user.group_set.all())).\ + if request.user.is_authenticated and join: + contests = contests.filter(Q(contest_type__in=[1, 2]) | Q(groups__in=request.user.group_set.all())). \ filter(end_time__gt=datetime.datetime.now(), start_time__lt=datetime.datetime.now()) - paginator = Paginator(contests, 20) try: current_page = paginator.page(int(page)) @@ -363,15 +375,10 @@ def contest_list_page(request, page=1): except Exception: pass - # 右侧的公告列表 - announcements = Announcement.objects.filter(is_global=True, visible=True).order_by("-create_time") - return render(request, "oj/contest/contest_list.html", {"contests": current_page, "page": int(page), "previous_page": previous_page, "next_page": next_page, - "keyword": keyword, "announcements": announcements, - "join": join}) - + "keyword": keyword, "join": join}) def _cmp(x, y): @@ -386,26 +393,85 @@ def _cmp(x, y): 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") - 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"] = {} - for item in submissions: - result[i]["submissions"][item.problem_id] = item - result[i]["total_ac"] = submissions.filter(ac=True).count() - result[i]["user"] = User.objects.get(id=result[i]["user_id"]) - result[i]["total_time"] = submissions.filter(ac=True).aggregate(total_time=Sum("total_time"))["total_time"] + 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 = [] return render(request, "oj/contest/contest_rank.html", - {"contest": contest, "contest_problems": contest_problems, "result": sorted(result, cmp=_cmp, reverse=True)}) - - - + {"contest": contest, "contest_problems": contest_problems, + "result": result, + "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}) +class ContestTimeAPIView(APIView): + """ + 获取比赛开始或者结束的倒计时,返回毫秒数字 + """ + def get(self, request): + t = request.GET.get("type", "start") + contest_id = request.GET.get("contest_id", -1) + try: + contest = Contest.objects.get(id=contest_id) + except Contest.DoesNotExist: + return error_response(u"比赛不存在") + if t == "start": + # 距离开始还有多长时间 + return success_response(int((contest.start_time - now()).total_seconds() * 1000)) + else: + # 距离结束还有多长时间 + return success_response(int((contest.end_time - now()).total_seconds() * 1000)) diff --git a/contest_submission/tests.py b/contest_submission/tests.py index 975d11c..cb44331 100644 --- a/contest_submission/tests.py +++ b/contest_submission/tests.py @@ -1,13 +1,15 @@ # coding=utf-8 import json + from django.test import TestCase, Client from django.core.urlresolvers import reverse -from account.models import User, REGULAR_USER, ADMIN, SUPER_ADMIN -from problem.models import Problem -from contest.models import Contest, ContestProblem -from submission.models import Submission from rest_framework.test import APITestCase, APIClient +from account.models import User, REGULAR_USER, ADMIN, SUPER_ADMIN +from contest.models import Contest, ContestProblem +from contest.models import PUBLIC_CONTEST, PASSWORD_PROTECTED_CONTEST +from submission.models import Submission + class ContestSubmissionAPITest(APITestCase): def setUp(self): @@ -20,7 +22,8 @@ class ContestSubmissionAPITest(APITestCase): self.user2.set_password("testbb") self.user2.save() self.global_contest = Contest.objects.create(title="titlex", description="descriptionx", mode=1, - contest_type=1, show_rank=True, show_user_submission=True, + contest_type=PUBLIC_CONTEST, show_rank=True, + show_user_submission=True, start_time="2015-08-15T10:00:00.000Z", end_time="2015-08-30T12:00:00.000Z", created_by=User.objects.get(username="test1")) @@ -70,7 +73,8 @@ class ContestProblemMySubmissionListTest(TestCase): self.user2.set_password("testbb") self.user2.save() self.global_contest = Contest.objects.create(title="titlex", description="descriptionx", mode=1, - contest_type=1, show_rank=True, show_user_submission=True, + contest_type=PUBLIC_CONTEST, show_rank=True, + show_user_submission=True, start_time="2015-08-15T10:00:00.000Z", end_time="2015-08-30T12:00:00.000Z", created_by=User.objects.get(username="test1")) @@ -104,7 +108,8 @@ class SubmissionAPITest(APITestCase): self.userS.set_password("testbb") self.userS.save() self.global_contest = Contest.objects.create(title="titlex", description="descriptionx", mode=1, - contest_type=2, show_rank=True, show_user_submission=True, + contest_type=PASSWORD_PROTECTED_CONTEST, show_rank=True, + show_user_submission=True, start_time="2015-08-15T10:00:00.000Z", end_time="2015-08-15T12:00:00.000Z", password="aacc", created_by=self.userS diff --git a/contest_submission/views.py b/contest_submission/views.py index d749fcf..59ee7fa 100644 --- a/contest_submission/views.py +++ b/contest_submission/views.py @@ -1,24 +1,24 @@ # coding=utf-8 import json - +from datetime import datetime import redis +import pytz from django.shortcuts import render from django.core.paginator import Paginator - +from django.utils import timezone 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 from contest.decorators import check_user_contest_permission - from problem.models import Problem from contest.models import Contest, ContestProblem from utils.shortcuts import serializer_invalid_response, error_response, success_response, error_page, paginate - from submission.models import Submission from .serializers import CreateContestSubmissionSerializer from submission.serializers import SubmissionSerializer @@ -43,20 +43,16 @@ class ContestSubmissionAPIView(APIView): problem.save() except ContestProblem.DoesNotExist: return error_response(u"题目不存在") - submission = Submission.objects.create(user_id=request.user.id, language=int(data["language"]), contest_id=contest.id, code=data["code"], problem_id=problem.id) try: judge.delay(submission.id, problem.time_limit, problem.memory_limit, problem.test_case_id) except Exception: return error_response(u"提交判题任务失败") - # 增加redis 中判题队列长度的计数器 r = redis.Redis(host=redis_config["host"], port=redis_config["port"], db=redis_config["db"]) r.incr("judge_queue_length") - return success_response({"submission_id": submission.id}) - else: return serializer_invalid_response(serializer) @@ -81,7 +77,7 @@ def contest_problem_my_submissions_list_page(request, contest_id, contest_proble {"submissions": submissions, "problem": contest_problem}) -@login_required +@check_user_contest_permission def contest_problem_submissions_list_page(request, contest_id, page=1): """ 单个比赛中的所有提交(包含自己和别人,自己可查提交结果,其他人不可查) @@ -90,10 +86,36 @@ def contest_problem_submissions_list_page(request, contest_id, page=1): contest = Contest.objects.get(id=contest_id) except Contest.DoesNotExist: return error_page(request, u"比赛不存在") - # 以下是本场比赛中所有的提交 - submissions = Submission.objects.filter(contest_id=contest_id). \ - values("id", "result", "create_time", "accepted_answer_time", "language", "user_id").order_by("-create_time") + + submissions = Submission.objects.filter(contest_id=contest_id).\ + values("id", "contest_id", "problem_id", "result", "create_time", + "accepted_answer_time", "language", "user_id").order_by("-create_time") + + + # 封榜的时候只能看到自己的提交 + if not contest.real_time_rank: + if not (request.user.admin_type == SUPER_ADMIN or request.user == contest.created_by): + submissions = submissions.filter(user_id=request.user.id) + + language = request.GET.get("language", None) + filter = None + if language: + submissions = submissions.filter(language=int(language)) + filter = {"name": "language", "content": language} + result = request.GET.get("result", None) + if result: + submissions = submissions.filter(result=int(result)) + filter = {"name": "result", "content": result} paginator = Paginator(submissions, 20) + + # 为查询题目标题创建新字典 + title = {} + contest_problems = ContestProblem.objects.filter(contest=contest) + for item in contest_problems: + title[item.id] = item.title + for item in submissions: + item['title'] = title[item['problem_id']] + try: current_page = paginator.page(int(page)) except Exception: @@ -108,10 +130,18 @@ def contest_problem_submissions_list_page(request, contest_id, page=1): except Exception: pass + for item in current_page: + # 自己提交的 管理员和创建比赛的可以看到所有的提交链接 + if item["user_id"] == request.user.id or request.user.admin_type == SUPER_ADMIN or \ + request.user == contest.created_by: + item["show_link"] = True + else: + item["show_link"] = False + return render(request, "oj/contest/submissions_list.html", {"submissions": current_page, "page": int(page), "previous_page": previous_page, "next_page": next_page, "start_id": int(page) * 20 - 20, - "contest": contest}) + "contest": contest, "filter": filter}) class ContestSubmissionAdminAPIView(APIView): @@ -144,6 +174,4 @@ class ContestSubmissionAdminAPIView(APIView): return error_response(u"参数错误!") if problem_id: submissions = submissions.filter(problem_id=problem_id) - return paginate(request, submissions, SubmissionSerializer) - diff --git a/daocloud.yml b/daocloud.yml index e5c5724..5e9f6ff 100644 --- a/daocloud.yml +++ b/daocloud.yml @@ -4,9 +4,9 @@ services: - mysql env: - - oj_env="daocloud" + - oj_env="local" script: - pip install -r requirements.txt - - mkdir LOG + - mkdir log - python manage.py test \ No newline at end of file diff --git a/judge/Dockerfile b/dockerfiles/judger/Dockerfile similarity index 91% rename from judge/Dockerfile rename to dockerfiles/judger/Dockerfile index c2ca1b1..6090728 100644 --- a/judge/Dockerfile +++ b/dockerfiles/judger/Dockerfile @@ -3,6 +3,8 @@ MAINTAINER virusdefender RUN mkdir /var/install/ WORKDIR /var/install/ ENV DEBIAN_FRONTEND noninteractive +RUN rm /etc/apt/sources.list +COPY sources.list /etc/apt/ RUN apt-get update RUN apt-get -y install software-properties-common python-software-properties RUN add-apt-repository -y ppa:webupd8team/java @@ -16,4 +18,5 @@ RUN git clone https://github.com/quark-zju/lrun.git RUN cd lrun && make install RUN mkdir -p /var/judger/run/ && mkdir /var/judger/test_case/ && mkdir /var/judger/code/ RUN chmod -R 777 /var/judger/run/ +COPY policy /var/judger/run/ WORKDIR /var/judger/code/ \ No newline at end of file diff --git a/dockerfiles/judger/policy b/dockerfiles/judger/policy new file mode 100644 index 0000000..a057b21 --- /dev/null +++ b/dockerfiles/judger/policy @@ -0,0 +1,3 @@ +grant { + permission java.io.FilePermission "/tmp", "read"; +}; \ No newline at end of file diff --git a/dockerfiles/judger/sources.list b/dockerfiles/judger/sources.list new file mode 100644 index 0000000..3f18a0f --- /dev/null +++ b/dockerfiles/judger/sources.list @@ -0,0 +1,10 @@ +deb http://mirrors.aliyun.com/ubuntu/ trusty main restricted universe multiverse +deb http://mirrors.aliyun.com/ubuntu/ trusty-security main restricted universe multiverse +deb http://mirrors.aliyun.com/ubuntu/ trusty-updates main restricted universe multiverse +deb http://mirrors.aliyun.com/ubuntu/ trusty-proposed main restricted universe multiverse +deb http://mirrors.aliyun.com/ubuntu/ trusty-backports main restricted universe multiverse +deb-src http://mirrors.aliyun.com/ubuntu/ trusty main restricted universe multiverse +deb-src http://mirrors.aliyun.com/ubuntu/ trusty-security main restricted universe multiverse +deb-src http://mirrors.aliyun.com/ubuntu/ trusty-updates main restricted universe multiverse +deb-src http://mirrors.aliyun.com/ubuntu/ trusty-proposed main restricted universe multiverse +deb-src http://mirrors.aliyun.com/ubuntu/ trusty-backports main restricted universe multiverse \ No newline at end of file diff --git a/Dockerfile b/dockerfiles/oj_web_server/Dockerfile similarity index 59% rename from Dockerfile rename to dockerfiles/oj_web_server/Dockerfile index 2e10830..07a73e9 100644 --- a/Dockerfile +++ b/dockerfiles/oj_web_server/Dockerfile @@ -1,7 +1,8 @@ FROM python:2.7 ENV PYTHONBUFFERED 1 -RUN mkdir -p /code/log /code/test_case +RUN mkdir -p /code/log /code/test_case /code/upload WORKDIR /code ADD requirements.txt /code/ RUN pip install -r requirements.txt -EXPOSE 8010 \ No newline at end of file +EXPOSE 8010 +CMD supervisord \ No newline at end of file diff --git a/dockerfiles/oj_web_server/gunicorn.conf b/dockerfiles/oj_web_server/gunicorn.conf new file mode 100644 index 0000000..3ad6496 --- /dev/null +++ b/dockerfiles/oj_web_server/gunicorn.conf @@ -0,0 +1,16 @@ +[program:gunicorn] + +command=gunicorn oj.wsgi:application -b 0.0.0.0:8080 --reload + +directory=/code/ +user=root +numprocs=1 +stdout_logfile=/code/log/gunicorn.log +stderr_logfile=/code/log/gunicorn.log +autostart=true +autorestart=true +startsecs=5 + +stopwaitsecs = 6 + +killasgroup=true \ No newline at end of file diff --git a/dockerfiles/oj_web_server/mq.conf b/dockerfiles/oj_web_server/mq.conf new file mode 100644 index 0000000..ae1797c --- /dev/null +++ b/dockerfiles/oj_web_server/mq.conf @@ -0,0 +1,16 @@ +[program:mq] + +command=python manage.py runscript mq + +directory=/code/ +user=root +numprocs=1 +stdout_logfile=/code/log/mq.log +stderr_logfile=/code/log/mq.log +autostart=true +autorestart=true +startsecs=5 + +stopwaitsecs = 6 + +killasgroup=true \ No newline at end of file diff --git a/requirements.txt b/dockerfiles/oj_web_server/requirements.txt similarity index 88% rename from requirements.txt rename to dockerfiles/oj_web_server/requirements.txt index 3a706b4..b22146d 100644 --- a/requirements.txt +++ b/dockerfiles/oj_web_server/requirements.txt @@ -8,4 +8,5 @@ celery gunicorn coverage django-extensions -supervisor \ No newline at end of file +supervisor +pillow \ No newline at end of file diff --git a/dockerfiles/oj_web_server/supervisord.conf b/dockerfiles/oj_web_server/supervisord.conf new file mode 100644 index 0000000..420b65f --- /dev/null +++ b/dockerfiles/oj_web_server/supervisord.conf @@ -0,0 +1,26 @@ +[unix_http_server] +file=/tmp/supervisor.sock ; path to your socket file + +[supervisord] +logfile=/code/log/supervisord.log ; supervisord log file +logfile_maxbytes=50MB ; maximum size of logfile before rotation +logfile_backups=10 ; number of backed up logfiles +loglevel=info ; info, debug, warn, trace +pidfile=/code/log/supervisord.pid ; pidfile location +nodaemon=true ; run supervisord as a daemon +minfds=1024 ; number of startup file descriptors +minprocs=200 ; number of process descriptors +user=root ; default user +childlogdir=/code/log/ ; where child log files will live + + +[rpcinterface:supervisor] +supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface + +[supervisorctl] +serverurl=unix:///tmp/supervisor.sock ; use unix:// schem for a unix sockets. + + +[include] + +files=gunicorn.conf mq.conf \ No newline at end of file diff --git a/group/views.py b/group/views.py index 2929706..a4a3123 100644 --- a/group/views.py +++ b/group/views.py @@ -254,9 +254,6 @@ class JoinGroupRequestAdminAPIView(APIView, GroupAPIViewBase): @login_required def group_list_page(request, page=1): - # 右侧的公告列表 - announcements = Announcement.objects.filter(is_global=True, visible=True).order_by("-create_time") - groups = Group.objects.filter(visible=True, join_group_setting__lte=2) # 搜索的情况 keyword = request.GET.get("keyword", None) @@ -282,10 +279,10 @@ def group_list_page(request, page=1): pass return render(request, "oj/group/group_list.html", { - "groups": groups, "announcements": announcements, + "groups": groups, "contests": current_page, "page": int(page), "previous_page": previous_page, "next_page": next_page, - "keyword": keyword, "announcements": announcements, + "keyword": keyword }) diff --git a/install/__init__.py b/install/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/install/migrations/__init__.py b/install/migrations/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/install/models.py b/install/models.py deleted file mode 100644 index 71a8362..0000000 --- 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 7ce503c..0000000 --- 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 75fd7bd..0000000 --- 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 c40444b..0000000 --- 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/client.py b/judge/judger/client.py index 4b0ec02..67073d8 100644 --- a/judge/judger/client.py +++ b/judge/judger/client.py @@ -1,4 +1,5 @@ # coding=utf-8 +import os import json import commands import hashlib @@ -7,9 +8,9 @@ from multiprocessing import Pool from settings import max_running_number, lrun_gid, lrun_uid, judger_workspace from language import languages from result import result -from compiler import compile_ -from judge_exceptions import JudgeClientError, CompileError +from judge_exceptions import JudgeClientError from utils import parse_lrun_output +from logger import logger # 下面这个函数作为代理访问实例变量,否则Python2会报错,是Python2的已知问题 @@ -61,6 +62,8 @@ class JudgeClient(object): " --max-real-time " + str(self._max_real_time / 1000.0 * 2) + \ " --max-memory " + str(self._max_memory * 1000 * 1000) + \ " --network false" + \ + " --syscalls '" + self._language["syscalls"] + "'" + \ + " --max-nprocess 20" + \ " --uid " + str(lrun_uid) + \ " --gid " + str(lrun_gid) @@ -82,6 +85,8 @@ class JudgeClient(object): # 倒序找到MEMORY的位置 output_start = output.rfind("MEMORY") if output_start == -1: + logger.error("Lrun result parse error") + logger.error(output) raise JudgeClientError("Lrun result parse error") # 如果不是0,说明lrun输出前面有输出,也就是程序的stderr有内容 if output_start != 0: @@ -92,26 +97,35 @@ class JudgeClient(object): return error, parse_lrun_output(output) def _compare_output(self, test_case_id): - test_case_md5 = self._test_case_info["test_cases"][str(test_case_id)]["output_md5"] + test_case_config = self._test_case_info["test_cases"][str(test_case_id)] output_path = judger_workspace + str(test_case_id) + ".out" try: f = open(output_path, "rb") except IOError: # 文件不存在等引发的异常 返回结果错误 - return False + return "", False - # 计算输出文件的md5 和之前测试用例文件的md5进行比较 - md5 = hashlib.md5() - while True: - data = f.read(2 ** 8) - if not data: - break - md5.update(data) + if "striped_output_md5" not in test_case_config: + # 计算输出文件的md5 和之前测试用例文件的md5进行比较 + # 兼容之前没有striped_output_md5的测试用例 + # 现在比较的是完整的文件 + md5 = hashlib.md5() + while True: + data = f.read(2 ** 8) + if not data: + break + md5.update(data) + output_md5 = md5.hexdigest() - # 对比文件是否一致 - # todo 去除最后的空行 - return md5.hexdigest() == test_case_md5 + return output_md5, output_md5 == test_case_config["output_md5"] + else: + # 这时候需要去除用户输出最后的空格和换行 再去比较md5 + md5 = hashlib.md5() + # 比较和返回去除空格后的md5比较结果 + md5.update(f.read().rstrip()) + output_md5 = md5.hexdigest() + return output_md5, output_md5 == test_case_config["striped_output_md5"] def _judge_one(self, test_case_id): # 运行lrun程序 接收返回值 @@ -123,26 +137,30 @@ class JudgeClient(object): run_result["test_case_id"] = test_case_id - # 如果返回值非0 或者信号量不是0 或者程序的stderr有输出 代表非正常结束 - if run_result["exit_code"] or run_result["term_sig"] or run_result["siginaled"] or error: - run_result["result"] = result["runtime_error"] - return run_result - - # 代表内存或者时间超过限制了 + # 代表内存或者时间超过限制了 程序被终止掉 要在runtime error 之前判断 if run_result["exceed"]: if run_result["exceed"] == "memory": run_result["result"] = result["memory_limit_exceeded"] elif run_result["exceed"] in ["cpu_time", "real_time"]: run_result["result"] = result["time_limit_exceeded"] else: + logger.error("Error exceeded type: " + run_result["exceed"]) + logger.error(output) raise JudgeClientError("Error exceeded type: " + run_result["exceed"]) return run_result + # 如果返回值非0 或者信号量不是0 或者程序的stderr有输出 代表非正常结束 + if run_result["exit_code"] or run_result["term_sig"] or run_result["siginaled"] or error: + run_result["result"] = result["runtime_error"] + return run_result + # 下面就是代码正常运行了 需要判断代码的输出是否正确 - if self._compare_output(test_case_id): + output_md5, r = self._compare_output(test_case_id) + if r: run_result["result"] = result["accepted"] else: run_result["result"] = result["wrong_answer"] + run_result["output_md5"] = output_md5 return run_result @@ -160,8 +178,8 @@ class JudgeClient(object): try: results.append(item.get()) except Exception as e: - # todo logging - print e + logger.error("system error") + logger.error(e) results.append({"result": result["system_error"]}) return results diff --git a/judge/judger/compiler.py b/judge/judger/compiler.py index 7f40ff1..127750b 100644 --- a/judge/judger/compiler.py +++ b/judge/judger/compiler.py @@ -4,6 +4,7 @@ import commands from settings import lrun_uid, lrun_gid from judge_exceptions import CompileError, JudgeClientError from utils import parse_lrun_output +from logger import logger def compile_(language_item, src_path, exe_path): @@ -22,14 +23,20 @@ def compile_(language_item, src_path, exe_path): output_start = output.rfind("MEMORY") if output_start == -1: + logger.error("Compiler error") + logger.error(output) raise JudgeClientError("Error running compiler in lrun") - # 返回值不为0 或者 stderr中lrun的输出之前有东西 - if status or output_start: + # 返回值不为 0 或者 stderr 中 lrun 的输出之前有 erro r字符串 + # 判断 error 字符串的原因是链接的时候可能会有一些不推荐使用的函数的的警告, + # 但是 -w 参数并不能关闭链接时的警告 + if status or "error" in output[0:output_start]: raise CompileError(output[0:output_start]) - parse_result = parse_lrun_output(output) + parse_result = parse_lrun_output(output[output_start:]) if parse_result["exit_code"] or parse_result["term_sig"] or parse_result["siginaled"] or parse_result["exceed"]: + logger.error("Compiler error") + logger.error(output) raise CompileError("Compile error") return exe_path diff --git a/judge/judger/language.py b/judge/judger/language.py index a61bd32..c7a14eb 100644 --- a/judge/judger/language.py +++ b/judge/judger/language.py @@ -6,6 +6,7 @@ languages = { "name": "c", "src_name": "main.c", "code": 1, + "syscalls": "!execve:k,flock:k,ptrace:k,sync:k,fdatasync:k,fsync:k,msync,sync_file_range:k,syncfs:k,unshare:k,setns:k,clone:k,query_module:k,sysinfo:k,syslog:k,sysfs:k", "compile_command": "gcc -DONLINE_JUDGE -O2 -w -std=c99 {src_path} -lm -o {exe_path}main", "execute_command": "{exe_path}main" }, @@ -13,6 +14,7 @@ languages = { "name": "cpp", "src_name": "main.cpp", "code": 2, + "syscalls": "!execve:k,flock:k,ptrace:k,sync:k,fdatasync:k,fsync:k,msync,sync_file_range:k,syncfs:k,unshare:k,setns:k,clone:k,query_module:k,sysinfo:k,syslog:k,sysfs:k", "compile_command": "g++ -DONLINE_JUDGE -O2 -w -std=c++11 {src_path} -lm -o {exe_path}main", "execute_command": "{exe_path}main" }, @@ -20,8 +22,9 @@ languages = { "name": "java", "src_name": "Main.java", "code": 3, + "syscalls": "!execve:k,flock:k,ptrace:k,sync:k,fdatasync:k,fsync:k,msync,sync_file_range:k,syncfs:k,unshare:k,setns:k,clone[a&268435456==268435456]:k,query_module:k,sysinfo:k,syslog:k,sysfs:k", "compile_command": "javac {src_path} -d {exe_path}", - "execute_command": "java -cp {exe_path} Main" + "execute_command": "java -cp {exe_path} -Djava.security.manager -Djava.security.policy==policy Main" } } diff --git a/judge/judger/logger.py b/judge/judger/logger.py new file mode 100644 index 0000000..c71a542 --- /dev/null +++ b/judge/judger/logger.py @@ -0,0 +1,8 @@ +# coding=utf-8 +import logging + +logging.basicConfig(level=logging.DEBUG, + format='%(asctime)s [%(threadName)s:%(thread)d] [%(name)s:%(lineno)d] [%(module)s:%(funcName)s] [%(levelname)s]- %(message)s', + filename='log/judge.log') + +logger = logging diff --git a/judge/judger/run.py b/judge/judger/run.py index 6ecc2d3..8aeb547 100644 --- a/judge/judger/run.py +++ b/judge/judger/run.py @@ -7,9 +7,8 @@ from client import JudgeClient from language import languages from compiler import compile_ from result import result -from settings import judger_workspace - -from settings import submission_db +from settings import judger_workspace, submission_db +from logger import logger # 简单的解析命令行参数 @@ -60,7 +59,6 @@ except Exception as e: conn.commit() exit() -print "Compile successfully" # 运行 try: client = JudgeClient(language_code=language_code, @@ -80,16 +78,13 @@ try: judge_result["accepted_answer_time"] = l[-1]["cpu_time"] except Exception as e: - print e + logger.error(e) conn = db_conn() cur = conn.cursor() cur.execute("update submission set result=%s, info=%s where id=%s", (result["system_error"], str(e), submission_id)) conn.commit() exit() -print "Run successfully" -print judge_result - conn = db_conn() cur = conn.cursor() cur.execute("update submission set result=%s, info=%s, accepted_answer_time=%s where id=%s", diff --git a/judge/judger/settings.py b/judge/judger/settings.py index 5f8d7ce..224cf5d 100644 --- a/judge/judger/settings.py +++ b/judge/judger/settings.py @@ -1,4 +1,5 @@ # coding=utf-8 +import os # 单个判题端最多同时运行的程序个数,因为判题端会同时运行多组测试数据,比如一共有5组测试数据 # 如果MAX_RUNNING_NUMBER大于等于5,那么这5组数据就会同时进行评测,然后返回结果。 # 如果MAX_RUNNING_NUMBER小于5,为3,那么就会同时运行前三组测试数据,然后再运行后两组数据 @@ -14,12 +15,10 @@ lrun_gid = 1002 # judger工作目录 judger_workspace = "/var/judger/" - -# 这个是在docker 中访问数据库 ip 不一定和web服务器还有celery的一样 submission_db = { - "host": "10.172.22.50",#"192.168.42.1", + "host": os.environ.get("MYSQL_PORT_3306_TCP_ADDR", "127.0.0.1"), "port": 3306, "db": "oj_submission", "user": "root", - "password": "mypwd" + "password": os.environ.get("MYSQL_ENV_MYSQL_ROOT_PASSWORD", "root") } diff --git a/judge/judger_controller/settings.py b/judge/judger_controller/settings.py index e8a7558..4d48340 100644 --- a/judge/judger_controller/settings.py +++ b/judge/judger_controller/settings.py @@ -1,7 +1,13 @@ # coding=utf-8 +""" +注意: +此文件包含 celery 的部分配置,但是 celery 并不是运行在docker 中的,所以本配置文件中的 redis和 MySQL 的地址就应该是 +运行 redis 和 MySQL 的 docker 容器的地址了。怎么获取这个地址见帮助文档。测试用例的路径和源代码路径同理。 +""" +import os # 这个redis 是 celery 使用的,包括存储队列信息还有部分统计信息 redis_config = { - "host": "127.0.0.1", + "host": os.environ.get("REDIS_PORT_6379_TCP_ADDR"), "port": 6379, "db": 0 } @@ -9,8 +15,7 @@ redis_config = { # 判题的 docker 容器的配置参数 docker_config = { - - "image_name": " 819d3da18dc1", + "image_name": "judger", "docker_path": "docker", "shell": True } @@ -19,12 +24,14 @@ docker_config = { # 测试用例的路径,是主机上的实际路径 test_case_dir = "/root/test_case/" # 源代码路径,也就是 manage.py 所在的实际路径 -source_code_dir = "/var/mnt/source/OnlineJudge/" +source_code_dir = "/root/qduoj/" +# 日志文件夹路径 +log_dir = "/root/log/" # 存储提交信息的数据库,是 celery 使用的,与 oj.settings/local_settings 等区分,那是 web 服务器访问的地址 submission_db = { - "host": "127.0.0.1", + "host": os.environ.get("submission_db_host"), "port": 3306, "db": "oj_submission", "user": "root", diff --git a/judge/judger_controller/tasks.py b/judge/judger_controller/tasks.py index cc3d7d5..2de71d7 100644 --- a/judge/judger_controller/tasks.py +++ b/judge/judger_controller/tasks.py @@ -5,32 +5,34 @@ import MySQLdb import subprocess from ..judger.result import result from ..judger_controller.celery import app -from settings import docker_config, source_code_dir, test_case_dir, submission_db, redis_config +from settings import docker_config, source_code_dir, test_case_dir, log_dir, submission_db, redis_config @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 --privileged --rm " \ + "--link mysql " \ "-v %s:/var/judger/test_case/ " \ "-v %s:/var/judger/code/ " \ + "-v %s:/var/judger/code/log/ " \ "%s " \ "python judge/judger/run.py " \ "--solution_id %s --time_limit %s --memory_limit %s --test_case_id %s" % \ (docker_config["docker_path"], test_case_dir, source_code_dir, + log_dir, docker_config["image_name"], submission_id, str(time_limit), str(memory_limit), test_case_id) subprocess.call(command, shell=docker_config["shell"]) except Exception as e: - print e conn = MySQLdb.connect(db=submission_db["db"], user=submission_db["user"], passwd=submission_db["password"], host=submission_db["host"], port=submission_db["port"], - character="utf8") + charset="utf8") cur = conn.cursor() cur.execute("update submission set result=%s, info=%s where id=%s", diff --git a/judge/tests/c/cpu_time_timeout.c b/judge/tests/c/cpu_time_timeout.c deleted file mode 100644 index 661050e..0000000 --- a/judge/tests/c/cpu_time_timeout.c +++ /dev/null @@ -1,12 +0,0 @@ -#include -int main() -{ - int a = 0; - int i = 0; - for(i = 0; i < 9999999999;i++) - { - a += i; - } - printf("%d", a); - return 0; -} \ No newline at end of file diff --git a/judge/tests/c/real_time_timeout.c b/judge/tests/c/real_time_timeout.c deleted file mode 100644 index 0051986..0000000 --- a/judge/tests/c/real_time_timeout.c +++ /dev/null @@ -1,6 +0,0 @@ -#include -#include -int main() -{ - -} \ No newline at end of file diff --git a/judge/tests/c/success.c b/judge/tests/c/success.c deleted file mode 100644 index aa0b2bb..0000000 --- a/judge/tests/c/success.c +++ /dev/null @@ -1,8 +0,0 @@ -# include -int main() -{ - int a, b; - scanf("%d %d", &a, &b); - printf("%d", a + b); - return 0; -} \ No newline at end of file diff --git a/mq/scripts/info.py b/mq/scripts/info.py deleted file mode 100644 index 119845c..0000000 --- a/mq/scripts/info.py +++ /dev/null @@ -1,93 +0,0 @@ -# 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 -from contest.models import ContestProblem, Contest, ContestSubmission - -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) - continue - - if submission.result == result["accepted"] and not submission.contest_id: - # 更新普通题目的 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) - # 普通题目的话,到这里就结束了 - continue - - # 能运行到这里的都是比赛题目 - try: - contest = Contest.objects.get(id=submission.contest_id) - contest_problem = ContestProblem.objects.get(contest=contest, id=submission.problem_id) - except Contest.DoesNotExist: - logger.warning("Submission contest does not exist, submission_id: " + submission_id) - continue - except ContestProblem.DoesNotExist: - 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 - contest_submission.total_submission_number += 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.total_time += int((submission.create_time - contest.start_time).total_seconds() / 60) - # 标记为已经通过 - contest_submission.ac = True - # contest problem ac 计数器加1 - contest_problem.total_accepted_number += 1 - else: - # 如果这个提交是错误的,就罚时20分钟 - contest_submission.total_time += 20 - contest_submission.save() - contest_problem.save() - except ContestSubmission.DoesNotExist: - # 第一次提交 - is_ac = submission.result == result["accepted"] - if is_ac: - total_time = int((submission.create_time - contest.start_time).total_seconds() / 60) - # 增加题目总的ac数计数器 - contest_problem.total_accepted_number += 1 - contest_problem.save() - else: - # 没过罚时20分钟 - total_time = 20 - ContestSubmission.objects.create(user_id=submission.user_id, contest=contest, problem=contest_problem, - ac=is_ac, total_time=total_time) - - -logger.debug("Start message queue") -MessageQueue().listen_task() diff --git a/mq/scripts/mq.py b/mq/scripts/mq.py new file mode 100644 index 0000000..567eebb --- /dev/null +++ b/mq/scripts/mq.py @@ -0,0 +1,114 @@ +# coding=utf-8 +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 +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") + + +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) + continue + + if submission.result == result["accepted"] and not submission.contest_id: + # 更新普通题目的 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) + 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) + user.save() + + # 普通题目的话,到这里就结束了 + continue + + # 能运行到这里的都是比赛题目 + try: + contest = Contest.objects.get(id=submission.contest_id) + contest_problem = ContestProblem.objects.get(contest=contest, id=submission.problem_id) + except Contest.DoesNotExist: + logger.warning("Submission contest does not exist, submission_id: " + submission_id) + continue + except ContestProblem.DoesNotExist: + 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() + 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) + +logger.debug("Start message queue") +MessageQueue().listen_task() diff --git a/oj/local_settings.py b/oj/local_settings.py index b923291..074b1b0 100644 --- a/oj/local_settings.py +++ b/oj/local_settings.py @@ -3,10 +3,6 @@ import os BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) - -# 下面是需要自己修改的 -LOG_PATH = "log/" - # 注意这是web 服务器访问的地址,判题端访问的地址不一定一样,因为可能不在一台机器上 DATABASES = { 'default': { @@ -17,17 +13,27 @@ DATABASES = { 'submission': { 'NAME': 'oj_submission', 'ENGINE': 'django.db.backends.mysql', - 'HOST': "121.42.32.129", + 'CONN_MAX_AGE': 0.1, + 'HOST': "127.0.0.1", 'PORT': 3306, 'USER': 'root', - 'PASSWORD': 'mypwd', - 'CONN_MAX_AGE': 0.1, + 'PASSWORD': 'root', } } +REDIS_CACHE = { + "host": "121.42.32.129", + "port": 6379, + "db": 1 +} + DEBUG = True -# 同理 这是 web 服务器的上传路径 -TEST_CASE_DIR = os.path.join(BASE_DIR, 'test_case/') - ALLOWED_HOSTS = [] + +# 在 debug 关闭的情况下,静态文件不是有 django runserver 来处理的,应该由 nginx 返回 +# 在 debug 开启的情况下,django 会在下面两个文件夹中寻找对应的静态文件。 +STATICFILES_DIRS = [os.path.join(BASE_DIR, "static/src/"), BASE_DIR] + +# 模板文件夹 +TEMPLATE_DIRS = [os.path.join(BASE_DIR, 'template/src/')] \ No newline at end of file diff --git a/oj/server_settings.py b/oj/server_settings.py index 185bf6b..77ca1fd 100644 --- a/oj/server_settings.py +++ b/oj/server_settings.py @@ -3,35 +3,41 @@ import os BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) - -# 下面是需要自己修改的 -LOG_PATH = "/var/log/oj/" - # 注意这是web 服务器访问的地址,判题端访问的地址不一定一样,因为可能不在一台机器上 DATABASES = { 'default': { 'ENGINE': 'django.db.backends.mysql', 'NAME': "oj", 'CONN_MAX_AGE': 0.1, - 'HOST': '127.0.0.1', + 'HOST': os.environ.get("MYSQL_PORT_3306_TCP_ADDR", "127.0.0.1"), 'PORT': 3306, 'USER': 'root', - 'PASSWORD': 'mypwd' + 'PASSWORD': os.environ.get("MYSQL_ENV_MYSQL_ROOT_PASSWORD", "root") }, 'submission': { 'NAME': 'oj_submission', 'ENGINE': 'django.db.backends.mysql', 'CONN_MAX_AGE': 0.1, - 'HOST': "127.0.0.1", + 'HOST': os.environ.get("MYSQL_PORT_3306_TCP_ADDR", "127.0.0.1"), 'PORT': 3306, 'USER': 'root', - 'PASSWORD': 'mypwd' + 'PASSWORD': os.environ.get("MYSQL_ENV_MYSQL_ROOT_PASSWORD", "root") } } -DEBUG = True +REDIS_CACHE = { + "host": os.environ.get("REDIS_PORT_6379_TCP_ADDR", "127.0.0.1"), + "port": 6379, + "db": 1 +} -# 同理 这是 web 服务器的上传路径 -TEST_CASE_DIR = '/root/test_case/' +DEBUG = False ALLOWED_HOSTS = ['*'] + +# 在 debug 关闭的情况下,静态文件不是有 django runserver 来处理的,应该由 nginx 返回 +# 在 debug 开启的情况下,django 会在下面两个文件夹中寻找对应的静态文件。 +STATICFILES_DIRS = [os.path.join(BASE_DIR, "static/release/"), os.path.join(BASE_DIR, "static/release/")] + +# 模板文件夹 +TEMPLATE_DIRS = [os.path.join(BASE_DIR, 'template/release/')] diff --git a/oj/settings.py b/oj/settings.py index 097a4c2..7e47919 100644 --- a/oj/settings.py +++ b/oj/settings.py @@ -32,8 +32,6 @@ BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) SECRET_KEY = 'hzfp^8mbgapc&x%$#xv)0=t8s7_ilingw(q3!@h&2fty6v6fxz' - - # Application definition INSTALLED_APPS = ( @@ -76,7 +74,7 @@ ROOT_URLCONF = 'oj.urls' TEMPLATES = [ { 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'DIRS': [os.path.join(BASE_DIR, 'template/src')], + 'DIRS': TEMPLATE_DIRS, 'APP_DIRS': True, 'OPTIONS': { 'context_processors': [ @@ -91,7 +89,6 @@ TEMPLATES = [ WSGI_APPLICATION = 'oj.wsgi.application' - # Internationalization # https://docs.djangoproject.com/en/1.8/topics/i18n/ @@ -111,10 +108,11 @@ USE_TZ = True STATIC_URL = '/static/' -STATICFILES_DIRS = (os.path.join(BASE_DIR, "static/src/"),) - AUTH_USER_MODEL = 'account.User' +LOG_PATH = "log/" + + LOGGING = { 'version': 1, 'disable_existing_loggers': True, @@ -166,4 +164,8 @@ REST_FRAMEWORK = { 'TEST_REQUEST_DEFAULT_FORMAT': 'json' } -DATABASE_ROUTERS = ['oj.db_router.DBRouter'] \ No newline at end of file +DATABASE_ROUTERS = ['oj.db_router.DBRouter'] + +TEST_CASE_DIR = os.path.join(BASE_DIR, 'test_case/') + +IMAGE_UPLOAD_DIR = os.path.join(BASE_DIR, 'upload/') \ No newline at end of file diff --git a/oj/urls.py b/oj/urls.py index 648c849..6f16acf 100644 --- a/oj/urls.py +++ b/oj/urls.py @@ -4,11 +4,12 @@ from django.views.generic import TemplateView from account.views import (UserLoginAPIView, UsernameCheckAPIView, UserRegisterAPIView, UserChangePasswordAPIView, EmailCheckAPIView, - UserAdminAPIView, UserInfoAPIView) + UserAdminAPIView, UserInfoAPIView, AccountSecurityAPIView) from announcement.views import AnnouncementAdminAPIView -from contest.views import ContestAdminAPIView, ContestProblemAdminAPIView, ContestPasswordVerifyAPIView +from contest.views import (ContestAdminAPIView, ContestProblemAdminAPIView, + ContestPasswordVerifyAPIView, ContestTimeAPIView) from group.views import (GroupAdminAPIView, GroupMemberAdminAPIView, JoinGroupAPIView, JoinGroupRequestAdminAPIView) @@ -16,16 +17,16 @@ from group.views import (GroupAdminAPIView, GroupMemberAdminAPIView, from admin.views import AdminTemplateView from problem.views import TestCaseUploadAPIView, ProblemTagAdminAPIView, ProblemAdminAPIView -from submission.views import SubmissionAPIView, SubmissionAdminAPIView +from submission.views import SubmissionAPIView, SubmissionAdminAPIView, SubmissionShareAPIView from contest_submission.views import ContestSubmissionAPIView, ContestSubmissionAdminAPIView from monitor.views import QueueLengthMonitorAPIView +from utils.views import SimditorImageUploadAPIView from contest_submission.views import contest_problem_my_submissions_list_page urlpatterns = [ - url(r'^install/$', "install.views.install"), - url("^$", TemplateView.as_view(template_name="oj/index.html"), name="index_page"), + url("^$", "account.views.index_page", name="index_page"), url(r'^docs/', include('rest_framework_swagger.urls')), url(r'^admin/$', TemplateView.as_view(template_name="admin/admin.html"), name="admin_spa_page"), url(r'^admin/contest/$', TemplateView.as_view(template_name="admin/contest/add_contest.html"), @@ -53,6 +54,7 @@ urlpatterns = [ url(r'^api/submission/$', SubmissionAPIView.as_view(), name="submission_api"), url(r'^api/group_join/$', JoinGroupAPIView.as_view(), name="group_join_api"), + url(r'^api/admin/upload_image/$', SimditorImageUploadAPIView.as_view(), name="simditor_upload_image"), url(r'^api/admin/announcement/$', AnnouncementAdminAPIView.as_view(), name="announcement_admin_api"), url(r'^api/admin/contest/$', ContestAdminAPIView.as_view(), name="contest_admin_api"), url(r'^api/admin/user/$', UserAdminAPIView.as_view(), name="user_admin_api"), @@ -106,5 +108,15 @@ urlpatterns = [ url(r'^groups/(?P\d+)/$', "group.views.group_list_page", name="group_list_page"), url(r'^group/(?P\d+)/$', "group.views.group_page", name="group_page"), url(r'^group/(?P\d+)/applications/$', "group.views.application_list_page", name="group_application_page"), - url(r'^group/application/(?P\d+)/$', "group.views.application_page", name="group_application") + url(r'^group/application/(?P\d+)/$', "group.views.application_page", name="group_application"), + + url(r'^about/$', TemplateView.as_view(template_name="utils/about.html"), name="about_page"), + url(r'^help/$', TemplateView.as_view(template_name="utils/help.html"), name="help_page"), + + url(r'^api/submission/share/$', SubmissionShareAPIView.as_view(), name="submission_share_api"), + + url(r'^captcha/$', "utils.captcha.views.show_captcha", name="show_captcha"), + url(r'^api/account_security_check/$', AccountSecurityAPIView.as_view(), name="account_security_check"), + + url(r'^api/contest/time/$', ContestTimeAPIView.as_view(), name="contest_time_api_view"), ] diff --git a/problem/serizalizers.py b/problem/serizalizers.py index f9c5caa..afa7f43 100644 --- a/problem/serizalizers.py +++ b/problem/serizalizers.py @@ -31,6 +31,7 @@ class CreateProblemSerializer(serializers.Serializer): difficulty = serializers.IntegerField() tags = serializers.ListField(child=serializers.CharField(max_length=10)) hint = serializers.CharField(max_length=3000, allow_blank=True) + visible = visible = serializers.BooleanField() class ProblemTagSerializer(serializers.ModelSerializer): diff --git a/problem/views.py b/problem/views.py index b73085f..dbe125a 100644 --- a/problem/views.py +++ b/problem/views.py @@ -13,12 +13,16 @@ from rest_framework.views import APIView from django.conf import settings + from announcement.models import Announcement from utils.shortcuts import (serializer_invalid_response, error_response, success_response, paginate, rand_str, error_page) from .serizalizers import (CreateProblemSerializer, EditProblemSerializer, ProblemSerializer, ProblemTagSerializer, CreateProblemTagSerializer) from .models import Problem, ProblemTag +import logging + +logger = logging.getLogger("app_info") def problem_page(request, problem_id): @@ -56,7 +60,8 @@ class ProblemAdminAPIView(APIView): memory_limit=data["memory_limit"], difficulty=data["difficulty"], created_by=request.user, - hint=data["hint"]) + hint=data["hint"], + visible=data["visible"]) for tag in data["tags"]: try: tag = ProblemTag.objects.get(name=tag) @@ -147,9 +152,13 @@ class TestCaseUploadAPIView(APIView): f = request.FILES["file"] tmp_zip = "/tmp/" + rand_str() + ".zip" - with open(tmp_zip, "wb") as test_case_zip: - for chunk in f: - test_case_zip.write(chunk) + try: + with open(tmp_zip, "wb") as test_case_zip: + for chunk in f: + test_case_zip.write(chunk) + except IOError as e: + logger.error(e) + return error_response(u"上传失败") test_case_file = zipfile.ZipFile(tmp_zip, 'r') name_list = test_case_file.namelist() @@ -198,16 +207,24 @@ class TestCaseUploadAPIView(APIView): # 计算输出文件的md5 for i in range(len(l) / 2): md5 = hashlib.md5() + striped_md5 = hashlib.md5() f = open(test_case_dir + str(i + 1) + ".out", "r") + # 完整文件的md5 while True: data = f.read(2 ** 8) if not data: break md5.update(data) + # 删除标准输出最后的空格和换行 + # 这时只能一次全部读入了,分块读的话,没办法确定文件结尾 + f.seek(0) + striped_md5.update(f.read().rstrip()) + file_info["test_cases"][str(i + 1)] = {"input_name": str(i + 1) + ".in", "output_name": str(i + 1) + ".out", "output_md5": md5.hexdigest(), + "striped_output_md5": striped_md5.hexdigest(), "output_size": os.path.getsize(test_case_dir + str(i + 1) + ".out")} # 写入配置文件 open(test_case_dir + "info", "w").write(json.dumps(file_info)) @@ -228,6 +245,17 @@ def problem_list_page(request, page=1): if keyword: problems = problems.filter(Q(title__contains=keyword) | Q(description__contains=keyword)) + difficulty_order = request.GET.get("order_by", None) + if difficulty_order: + if difficulty_order[0] == "-": + problems = problems.order_by("-difficulty") + difficulty_order = "difficulty" + else: + problems = problems.order_by("difficulty") + difficulty_order = "-difficulty" + else: + difficulty_order = "difficulty" + # 按照标签筛选 tag_text = request.GET.get("tag", None) if tag_text: @@ -235,7 +263,7 @@ def problem_list_page(request, page=1): tag = ProblemTag.objects.get(name=tag_text) except ProblemTag.DoesNotExist: return error_page(request, u"标签不存在") - problems = tag.problem_set.all() + problems = tag.problem_set.all().filter(visible=True) paginator = Paginator(problems, 20) try: @@ -255,13 +283,15 @@ def problem_list_page(request, page=1): except Exception: pass - # 右侧的公告列表 - announcements = Announcement.objects.filter(is_global=True, visible=True).order_by("-create_time") + 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, - "announcements": announcements, "tags": tags}) + "keyword": keyword, "tag": tag_text,"problems_status": problems_status, + "tags": tags, "difficulty_order": difficulty_order}) diff --git a/static/release/fis-conf.js b/static/release/fis-conf.js deleted file mode 100644 index ce128cd..0000000 --- a/static/release/fis-conf.js +++ /dev/null @@ -1,3 +0,0 @@ -/** - * Created by virusdefender on 8/25/15. - */ diff --git a/static/src/css/admin.css b/static/src/css/admin.css index fb9c9f2..6a613e7 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 56c87eb..0e7d357 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,17 @@ 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; + background-color: white; +} + +.CodeMirror-code{ + font-family: "Consolas", "Bitstream Vera Sans Mono", "Courier New", Courier, monospace; + background-color: white; +} diff --git a/static/src/css/oj.css b/static/src/css/oj.css index e099835..cecc0b5 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; @@ -73,6 +72,10 @@ li.list-group-item { color: green; } +.dealing-flag { + color: #FF9933; +} + .CodeMirror{ min-height: 250px; _height:250px; @@ -91,4 +94,21 @@ li.list-group-item { .contest-tab{ margin-bottom: 15px; -} \ No newline at end of file +} + +#share-code{ + margin-top: 15px; +} + +#share-code textarea { + margin-top: 15px; +} + +#about-acm-logo{ + width: 40%; +} + +.rank .first-achieved{ + background: #33CC99; +} + diff --git a/static/src/img/ZeroClipboard.swf b/static/src/img/ZeroClipboard.swf new file mode 100755 index 0000000..9c2171a Binary files /dev/null and b/static/src/img/ZeroClipboard.swf differ diff --git a/static/src/img/acm_logo.png b/static/src/img/acm_logo.png new file mode 100644 index 0000000..ce1cfed Binary files /dev/null and b/static/src/img/acm_logo.png differ diff --git a/static/src/img/chrome.png b/static/src/img/chrome.png new file mode 100644 index 0000000..06a52bc Binary files /dev/null and b/static/src/img/chrome.png differ diff --git a/static/src/img/favicon.ico b/static/src/img/favicon.ico new file mode 100644 index 0000000..6096ac4 Binary files /dev/null and b/static/src/img/favicon.ico differ diff --git a/static/src/img/firefox.png b/static/src/img/firefox.png new file mode 100644 index 0000000..2521bcb 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 0000000..1375a29 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 0000000..6d4fd6c --- /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/static/src/js/app/admin/admin.js b/static/src/js/app/admin/admin.js index e3e8545..878b9ca 100644 --- a/static/src/js/app/admin/admin.js +++ b/static/src/js/app/admin/admin.js @@ -1,4 +1,4 @@ -define("admin", ["jquery", "avalon"], function ($, avalon) { +require(["jquery", "avalon", "bootstrap"], function ($, avalon) { avalon.ready(function () { diff --git a/static/src/js/app/admin/contest/addContest.js b/static/src/js/app/admin/contest/addContest.js index a87e4e2..8aa5354 100644 --- a/static/src/js/app/admin/contest/addContest.js +++ b/static/src/js/app/admin/contest/addContest.js @@ -2,40 +2,46 @@ require(["jquery", "avalon", "editor", "uploader", "bsAlert", "csrfToken", "date "validator"], function ($, avalon, editor, uploader, bsAlert, csrfTokenHeader) { - //avalon.vmodels.add_contest = null; $("#add-contest-form").validator().on('submit', function (e) { - if (!e.isDefaultPrevented()){ + if (!e.isDefaultPrevented()) { e.preventDefault(); var ajaxData = { title: vm.title, description: vm.description, mode: vm.mode, contest_type: 0, - show_rank: vm.showRank, + real_time_rank: vm.realTimeRank, show_user_submission: vm.showSubmission, start_time: vm.startTime, end_time: vm.endTime, visible: false }; - if (vm.choseGroupList.length == 0) { - bsAlert("你没有选择参赛用户!"); - return false; + + var selectedGroups = []; + if (!vm.isGlobal) { + for (var i = 0; i < vm.allGroups.length; i++) { + if (vm.allGroups[i].isSelected) { + selectedGroups.push(vm.allGroups[i].id); + } + } + ajaxData.groups = selectedGroups; } - if (vm.choseGroupList[0].id == 0) { //everyone | public contest + else { if (vm.password) { ajaxData.password = vm.password; ajaxData.contest_type = 2; } - else{ + else ajaxData.contest_type = 1; - } } - else { // Add groups info - ajaxData.groups = []; - for (var i = 0; vm.choseGroupList[i]; i++) - ajaxData.groups.push(parseInt(vm.choseGroupList[i].id)) + if (!vm.isGlobal && !selectedGroups.length) { + bsAlert("你没有选择参赛用户!"); + return false; + } + if (vm.editDescription == "") { + bsAlert("比赛描述不能为空!"); + return false; } - $.ajax({ // Add contest beforeSend: csrfTokenHeader, url: "/api/admin/contest/", @@ -45,20 +51,18 @@ require(["jquery", "avalon", "editor", "uploader", "bsAlert", "csrfToken", "date method: "post", success: function (data) { if (!data.code) { - bsAlert("添加成功!将转到比赛列表页以便为比赛添加问题(注意比赛当前状态为:隐藏)"); - vm.title = ""; - vm.description = ""; - vm.startTime = ""; - vm.endTime = ""; - vm.password = ""; - vm.mode = ""; - vm.showRank = false; - vm.showSubmission = false; - vm.group = "-1"; - vm.groupList = []; - vm.choseGroupList = []; - vm.passwordUsable = false; - location.hash = "#contest/contest_list"; + bsAlert("添加成功!将转到比赛列表页以便为比赛添加问题(注意比赛当前状态为:隐藏)"); + vm.title = ""; + vm.description = ""; + vm.startTime = ""; + vm.endTime = ""; + vm.password = ""; + vm.mode = "0"; + vm.showSubmission = true; + location.hash = "#contest/contest_list"; + vm.isGlobal = true; + vm.allGroups = []; + vm.showGlobalViewRadio = true; } else { bsAlert(data.data); @@ -70,80 +74,59 @@ require(["jquery", "avalon", "editor", "uploader", "bsAlert", "csrfToken", "date }); editor("#editor"); - if (avalon.vmodels.add_contest) - var vm = avalon.vmodels.add_contest; - else - var vm = avalon.define({ - $id: "add_contest", - title: "", - description: "", - startTime: "", - endTime: "", - password: "", - mode: "", - showRank: false, - showSubmission: false, - group: "-1", - groupList: [], - choseGroupList: [], - passwordUsable: false, - addGroup: function() { - if (vm.group == -1) return; - if (vm.groupList[vm.group].id == 0){ - vm.passwordUsable = true; - vm.choseGroupList = []; - for (var key in vm.groupList){ - vm.groupList[key].chose = true; - } - } - vm.groupList[vm.group]. chose = true; - vm.choseGroupList.push({name:vm.groupList[vm.group].name, index:vm.group, id:vm.groupList[vm.group].id}); - vm.group = -1; - }, - removeGroup: function(groupIndex){ - if (vm.groupList[vm.choseGroupList[groupIndex].index].id == 0){ - vm.passwordUsable = false; - for (key in vm.groupList){ - vm.groupList[key].chose = false; - } - } - vm.groupList[vm.choseGroupList[groupIndex].index].chose = false; - vm.choseGroupList.remove(vm.choseGroupList[groupIndex]); - } - }); + if (avalon.vmodels.add_contest) + var vm = avalon.vmodels.add_contest; + else + var vm = avalon.define({ + $id: "add_contest", + title: "", + description: "", + startTime: "", + endTime: "", + password: "", + mode: "0", + showSubmission: true, + isGlobal: true, + allGroups: [], + showGlobalViewRadio: true, + realTimeRank: true + }); - $.ajax({ // Get current user type + $.ajax({ url: "/api/user/", method: "get", dataType: "json", success: function (data) { if (!data.code) { - if (data.data.admin_type == 2) { // Is super user - vm.isGlobal = true; - vm.groupList.push({id:0,name:"所有人",chose:false}); + var admin_type = data.data.admin_type; + if (data.data.admin_type == 1) { + vm.isGlobal = false; + vm.showGlobalViewRadio = false; + } - $.ajax({ // Get the group list of current user - beforeSend: csrfTokenHeader, - url: "/api/admin/group/", - method: "get", - dataType: "json", - success: function (data) { - if (!data.code) { - if (!data.data.length) { - return; - } - for (var i = 0; i < data.data.length; i++) { - var item = data.data[i]; - item["chose"] = false; - vm.groupList.push(item); - } + } + $.ajax({ + url: "/api/admin/group/", + method: "get", + dataType: "json", + success: function (data) { + if (!data.code) { + if (!data.data.length) { + if (admin_type != 2) + bsAlert("您的用户权限只能创建小组内比赛,但是您还没有创建过小组"); + return; } - else { - bsAlert(data.data); + for (var i = 0; i < data.data.length; i++) { + var item = data.data[i]; + item["isSelected"] = false; + vm.allGroups.push(item); } } - }); - } + else { + bsAlert(data.data); + } + } + }); } }); diff --git a/static/src/js/app/admin/contest/contestList.js b/static/src/js/app/admin/contest/contestList.js index c920cd6..c0f30af 100644 --- a/static/src/js/app/admin/contest/contestList.js +++ b/static/src/js/app/admin/contest/contestList.js @@ -3,21 +3,39 @@ require(["jquery", "avalon", "csrfToken", "bsAlert", "editor", "datetimePicker", avalon.ready(function () { $("#edit-contest-form").validator().on('submit', function (e) { - if (!e.isDefaultPrevented()){ + if (!e.isDefaultPrevented()) { e.preventDefault(); var ajaxData = { - id: vm.contestList[vm.editingContestId-1].id, - title: vm.editTitle, - description: vm.editDescription, - mode: vm.editMode, - contest_type: 0, - show_rank: vm.editShowRank, + id: vm.contestList[vm.editingContestId - 1].id, + title: vm.editTitle, + description: vm.editDescription, + mode: vm.editMode, + contest_type: 0, + real_time_rank: vm.editRealTimeRank, show_user_submission: vm.editShowSubmission, - start_time: vm.editStartTime, - end_time: vm.editEndTime, - visible: vm.editVisible + start_time: vm.editStartTime, + end_time: vm.editEndTime, + visible: vm.editVisible }; - if (vm.choseGroupList.length == 0) { + + var selectedGroups = []; + if (!vm.isGlobal) { + for (var i = 0; i < vm.allGroups.length; i++) { + if (vm.allGroups[i].isSelected) { + selectedGroups.push(vm.allGroups[i].id); + } + } + ajaxData.groups = selectedGroups; + } + else { + if (vm.editPassword) { + ajaxData.password = vm.editPassword; + ajaxData.contest_type = 2; + } + else + ajaxData.contest_type = 1; + } + if (!vm.isGlobal && !selectedGroups.length) { bsAlert("你没有选择参赛用户!"); return false; } @@ -25,22 +43,8 @@ require(["jquery", "avalon", "csrfToken", "bsAlert", "editor", "datetimePicker", bsAlert("比赛描述不能为空!"); return false; } - if (vm.choseGroupList[0].id == 0) { //everyone | public contest - if (vm.editPassword) { - ajaxData.password = vm.editPassword; - ajaxData.contest_type = 2; - } - else{ - ajaxData.contest_type = 1; - } - } - else { // Add groups info - ajaxData.groups = []; - for (var i = 0; vm.choseGroupList[i]; i++) - ajaxData.groups.push(parseInt(vm.choseGroupList[i].id)) - } - $.ajax({ // Add contest + $.ajax({ // modify contest info beforeSend: csrfTokenHeader, url: "/api/admin/contest/", dataType: "json", @@ -52,7 +56,7 @@ require(["jquery", "avalon", "csrfToken", "bsAlert", "editor", "datetimePicker", if (!data.code) { bsAlert("修改成功!"); vm.editingContestId = 0; // Hide the editor - vm.getPage(1); // Refresh the contest list + vm.getPage(1); // Refresh the contest list } else { bsAlert(data.data); @@ -63,138 +67,124 @@ require(["jquery", "avalon", "csrfToken", "bsAlert", "editor", "datetimePicker", return false; }); - if(avalon.vmodels.contestList){ - // this page has been loaded before, so set the default value - var vm = avalon.vmodels.contestList; - vm.contestList= []; - vm.previousPage= 0; - vm.nextPage= 0; - vm.page= 1; - vm.totalPage= 1; - vm.group= "-1"; - vm.groupList= []; - vm.choseGroupList= []; - vm.passwordUsable= false; - vm.keyword= ""; - vm.editingContestId= 0; - vm.editTitle= ""; - vm.editDescription= ""; - vm.editProblemList= []; - vm.editPassword= ""; - vm.editStartTime= ""; - vm.editEndTime= ""; - vm.editMode= ""; - vm.editShowRank= false; - vm.editShowSubmission= false; - vm.editProblemList= []; - vm.editVisible= false; - vm.editChoseGroupList= []; - vm.editingProblemContestIndex= 0; - } - else { - var vm = avalon.define({ - $id: "contestList", - contestList: [], - previousPage: 0, - nextPage: 0, - page: 1, - totalPage: 1, - showVisibleOnly: false, - group: "-1", - groupList: [], - choseGroupList: [], - passwordUsable: false, - keyword: "", - editingContestId: 0, - editTitle: "", - editDescription: "", - editProblemList: [], - editPassword: "", - editStartTime: "", - editEndTime: "", - editMode: "", - editShowRank: false, - editShowSubmission: false, - editProblemList: [], - editVisible: false, - editChoseGroupList: [], - editingProblemContestIndex: 0, - getNext: function () { - if (!vm.nextPage) - return; - getPageData(vm.page + 1); - }, - getPrevious: function () { - if (!vm.previousPage) - return; - getPageData(vm.page - 1); - }, - getBtnClass: function (btn) { - if (btn == "next") { - return vm.nextPage ? "btn btn-primary" : "btn btn-primary disabled"; - } - else { - return vm.previousPage ? "btn btn-primary" : "btn btn-primary disabled"; - } - }, - getPage: function (page_index) { - getPageData(page_index); - }, - showEditContestArea: function (contestId) { - if (vm.editingContestId && !confirm("如果继续将丢失未保存的信息,是否继续?")) - return; - if (contestId == vm.editingContestId) - vm.editingContestId = 0; - else { - vm.editingContestId = contestId; - vm.editTitle = vm.contestList[contestId-1].title; - vm.editPassword = vm.contestList[contestId-1].password; - vm.editStartTime = vm.contestList[contestId-1].start_time.substring(0,16).replace("T"," "); - vm.editEndTime = vm.contestList[contestId-1].end_time.substring(0,16).replace("T"," "); - vm.editMode = vm.contestList[contestId-1].mode; - vm.editVisible = vm.contestList[contestId-1].visible; - if (vm.contestList[contestId-1].contest_type == 0) { //contest type == 0, contest in group - //Clear the choseGroupList - while (vm.choseGroupList.length) { - vm.removeGroup(0); - } - - for (var i = 0; i < vm.contestList[contestId-1].groups.length; i++){ - var id = parseInt(vm.contestList[contestId-1].groups[i]); - var index = 0; - for (; vm.groupList[index]; index++) { - if (vm.groupList[index].id == id) - break; + if (avalon.vmodels.contestList) { + // this page has been loaded before, so set the default value + var vm = avalon.vmodels.contestList; + vm.contestList = []; + vm.previousPage = 0; + vm.nextPage = 0; + vm.page = 1; + vm.totalPage = 1; + vm.keyword = ""; + vm.editingContestId = 0; + vm.editTitle = ""; + vm.editDescription = ""; + vm.editProblemList = []; + vm.editPassword = ""; + vm.editStartTime = ""; + vm.editEndTime = ""; + vm.editMode = ""; + vm.editShowSubmission = false; + vm.editVisible = false; + vm.editingProblemContestIndex = 0; + vm.editRealTimeRank = true; + } + else { + var vm = avalon.define({ + $id: "contestList", + contestList: [], + previousPage: 0, + nextPage: 0, + page: 1, + totalPage: 1, + showVisibleOnly: false, + keyword: "", + editingContestId: 0, + editTitle: "", + editDescription: "", + editProblemList: [], + editPassword: "", + editStartTime: "", + editEndTime: "", + editMode: "", + editShowSubmission: false, + editVisible: false, + editRealTimeRank: true, + editingProblemContestIndex: 0, + isGlobal: true, + allGroups: [], + showGlobalViewRadio: true, + admin_type: 1, + getNext: function () { + if (!vm.nextPage) + return; + getPageData(vm.page + 1); + }, + getPrevious: function () { + if (!vm.previousPage) + return; + getPageData(vm.page - 1); + }, + getBtnClass: function (btn) { + if (btn == "next") { + return vm.nextPage ? "btn btn-primary" : "btn btn-primary disabled"; + } + else { + return vm.previousPage ? "btn btn-primary" : "btn btn-primary disabled"; + } + }, + getPage: function (page_index) { + getPageData(page_index); + }, + showEditContestArea: function (contestId) { + if (vm.editingContestId && !confirm("如果继续将丢失未保存的信息,是否继续?")) + return; + if (contestId == vm.editingContestId) + vm.editingContestId = 0; + else { + vm.editingContestId = contestId; + vm.editTitle = vm.contestList[contestId - 1].title; + vm.editPassword = vm.contestList[contestId - 1].password; + vm.editStartTime = vm.contestList[contestId - 1].start_time.substring(0, 16).replace("T", " "); + vm.editEndTime = vm.contestList[contestId - 1].end_time.substring(0, 16).replace("T", " "); + vm.editMode = vm.contestList[contestId - 1].mode; + vm.editVisible = vm.contestList[contestId - 1].visible; + vm.editRealTimeRank = vm.contestList[contestId - 1].real_time_rank; + if (vm.contestList[contestId - 1].contest_type == 0) { //contest type == 0, contest in group + vm.isGlobal = false; + for (var i = 0; i < vm.allGroups.length; i++) { + vm.allGroups[i].isSelected = false; + } + for (var i = 0; i < vm.contestList[contestId - 1].groups.length; i++) { + var id = parseInt(vm.contestList[contestId - 1].groups[i]); + + for (var index = 0; vm.allGroups[index]; index++) { + if (vm.allGroups[index].id == id) { + vm.allGroups[index].isSelected = true; + break; + } + } } - vm.groupList[index].chose = true; - vm.choseGroupList.push({ - name:vm.groupList[index].name, - index:index, - id:id - }); } + else { + vm.isGlobal = true; + } + vm.editShowSubmission = vm.contestList[contestId - 1].show_user_submission; + editor("#editor").setValue(vm.contestList[contestId - 1].description); + vm.editingProblemContestIndex = 0; } - else{ - vm.group = "0"; - vm.addGroup()//vm.editChoseGroupList = [0]; id 0 is for the group of everyone~ + }, + showEditProblemArea: function (contestId) { + if (vm.editingProblemContestIndex == contestId) { + vm.editingProblemContestIndex = 0; + return; } - vm.editShowRank = vm.contestList[contestId-1].show_rank; - vm.editShowSubmission = vm.contestList[contestId-1].show_user_submission; - editor("#editor").setValue(vm.contestList[contestId-1].description); - vm.editingProblemContestIndex = 0; - } - }, - showEditProblemArea: function(contestId) { - if (vm.editingProblemContestIndex == contestId) { - vm.editingProblemContestIndex = 0; - return; - } - if (vm.editingContestId&&!confirm("如果继续将丢失未保存的信息,是否继续?")){ - return; - } - $.ajax({ // Get the problem list of current contest + if (vm.editingContestId && !confirm("如果继续将丢失未保存的信息,是否继续?")) { + return; + } + $.ajax({ // Get the problem list of current contest beforeSend: csrfTokenHeader, - url: "/api/admin/contest_problem/?contest_id=" + vm.contestList[contestId-1].id, + url: "/api/admin/contest_problem/?contest_id=" + vm.contestList[contestId - 1].id, method: "get", dataType: "json", success: function (data) { @@ -206,51 +196,60 @@ require(["jquery", "avalon", "csrfToken", "bsAlert", "editor", "datetimePicker", } } }); - vm.editingContestId = 0; - vm.editingProblemContestIndex = contestId; - vm.editMode = vm.contestList[contestId-1].mode; - }, - addGroup: function() { - if (vm.group == -1) return; - if (vm.groupList[vm.group].id == 0){ - vm.passwordUsable = true; - vm.choseGroupList = []; - for (var i = 0; i < vm.groupList.length; i++) { - vm.groupList[i].chose = true; - } - } - vm.groupList[vm.group]. chose = true; - // index of the group is relative. It is related to user - vm.choseGroupList.push({name:vm.groupList[vm.group].name, index:vm.group, id:vm.groupList[vm.group].id}); - vm.group = -1; - }, - removeGroup: function(groupIndex){ - if (vm.groupList[vm.choseGroupList[groupIndex].index].id == 0){ - vm.passwordUsable = false; - for (var i = 0; i < vm.groupList.length; i++) { - vm.groupList[i].chose = false; - } - } - vm.groupList[vm.choseGroupList[groupIndex].index].chose = false; - vm.choseGroupList.remove(vm.choseGroupList[groupIndex]); + vm.editingContestId = 0; + vm.editingProblemContestIndex = contestId; + vm.editMode = vm.contestList[contestId - 1].mode; }, - addProblem: function () { - vm.$fire("up!showContestProblemPage", 0, vm.contestList[vm.editingProblemContestIndex-1].id, vm.editMode); - }, - showProblemEditPage: function(el) { - vm.$fire("up!showContestProblemPage", el.id, vm.contestList[vm.editingProblemContestIndex-1].id, vm.editMode); - }, - showSubmissionPage: function(el) { - var problemId = 0 - if (el) - problemId = el.id; - vm.$fire("up!showContestSubmissionPage", problemId, vm.contestList[vm.editingProblemContestIndex-1].id, vm.editMode); - } - }); - vm.$watch("showVisibleOnly", function() { - getPageData(1); - }) - } + addProblem: function () { + vm.$fire("up!showContestProblemPage", 0, vm.contestList[vm.editingProblemContestIndex - 1].id, vm.editMode); + }, + showProblemEditPage: function (el) { + vm.$fire("up!showContestProblemPage", el.id, vm.contestList[vm.editingProblemContestIndex - 1].id, vm.editMode); + }, + showSubmissionPage: function (el) { + var problemId = 0 + if (el) + problemId = el.id; + vm.$fire("up!showContestSubmissionPage", problemId, vm.contestList[vm.editingProblemContestIndex - 1].id, vm.editMode); + }, + addToProblemList: function (problem) { + var ajaxData = { + title: problem.title, + description: problem.description, + time_limit: problem.time_limit, + memory_limit: problem.memory_limit, + samples: problem.samples, + test_case_id: problem.test_case_id, + hint: problem.hint, + source: problem.contest.title, + visible: false, + tags: [], + input_description: problem.input_description, + output_description: problem.output_description, + difficulty: 0 + }; + $.ajax({ + beforeSend: csrfTokenHeader, + url: "/api/admin/problem/", + dataType: "json", + data: JSON.stringify(ajaxData), + method: "post", + contentType: "application/json", + success: function (data) { + if (!data.code) { + bsAlert("题目添加成功!题目现在处于隐藏状态,请到题目列表手动修改,并添加分类和难度信息!"); + } + else { + bsAlert(data.data); + } + } + }); + } + }); + vm.$watch("showVisibleOnly", function () { + getPageData(1); + }) + } getPageData(1); //init time picker @@ -293,39 +292,42 @@ require(["jquery", "avalon", "csrfToken", "bsAlert", "editor", "datetimePicker", } // Get group list - $.ajax({ // Get current user type + $.ajax({ url: "/api/user/", method: "get", dataType: "json", success: function (data) { if (!data.code) { - if (data.data.admin_type == 2) { // Is super user - vm.isGlobal = true; - vm.groupList.push({id:0,name:"所有人",chose:false}); + var admin_type = data.data.admin_type; + vm.admin_type = admin_type; + if (data.data.admin_type == 1) { + vm.isGlobal = false; + vm.showGlobalViewRadio = false; } - $.ajax({ // Get the group list of current user - beforeSend: csrfTokenHeader, - url: "/api/admin/group/", - method: "get", - dataType: "json", - success: function (data) { - if (!data.code) { - if (!data.data.length) { - //this user have no group can use - return; - } - for (var i = 0; i < data.data.length; i++) { - var item = data.data[i]; - item["chose"] = false; - vm.groupList.push(item); - } + } + $.ajax({ + url: "/api/admin/group/", + method: "get", + dataType: "json", + success: function (data) { + if (!data.code) { + if (!data.data.length) { + + if (admin_type != 2) + bsAlert("您的用户权限只能创建小组内比赛,但是您还没有创建过小组"); + return; } - else { - bsAlert(data.data); + for (var i = 0; i < data.data.length; i++) { + var item = data.data[i]; + item["isSelected"] = false; + vm.allGroups.push(item); } } - }); - } + else { + bsAlert(data.data); + } + } + }); } }); diff --git a/static/src/js/app/admin/problem/addProblem.js b/static/src/js/app/admin/problem/addProblem.js index c6e49ca..103396b 100644 --- a/static/src/js/app/admin/problem/addProblem.js +++ b/static/src/js/app/admin/problem/addProblem.js @@ -13,6 +13,10 @@ require(["jquery", "avalon", "editor", "uploader", "bsAlert", "csrfToken", "tagE bsAlert("题目描述不能为空!"); return false; } + if (vm.timeLimit < 100 || vm.timeLimit > 5000) { + bsAlert("保证时间限制是一个100-5000的合法整数"); + return false; + } if (vm.samples.length == 0) { bsAlert("请至少添加一组样例!"); return false; diff --git a/static/src/js/app/admin/problem/editProblem.js b/static/src/js/app/admin/problem/editProblem.js index a746d7a..416d86f 100644 --- a/static/src/js/app/admin/problem/editProblem.js +++ b/static/src/js/app/admin/problem/editProblem.js @@ -14,8 +14,8 @@ require(["jquery", "avalon", "editor", "uploader", "bsAlert", "csrfToken", "tagE bsAlert("题目描述不能为空!"); return false; } - if (vm.timeLimit < 1000 || vm.timeLimit > 5000) { - bsAlert("保证时间限制是一个1000-5000的合法整数"); + if (vm.timeLimit < 100 || vm.timeLimit > 5000) { + bsAlert("保证时间限制是一个100-5000的合法整数"); return false; } if (vm.samples.length == 0) { diff --git a/static/src/js/app/oj/account/change_password.js b/static/src/js/app/oj/account/changePassword.js similarity index 70% rename from static/src/js/app/oj/account/change_password.js rename to static/src/js/app/oj/account/changePassword.js index 608114f..37c3ec3 100644 --- a/static/src/js/app/oj/account/change_password.js +++ b/static/src/js/app/oj/account/changePassword.js @@ -1,13 +1,22 @@ require(["jquery", "bsAlert", "csrfToken", "validator"], function ($, bsAlert, csrfTokenHeader) { + function refresh_captcha(){ + $("#captcha-img")[0].src = "/captcha/?" + Math.random(); + $("#captcha")[0].value = ""; + } + $("#captcha-img").click(function(){ + refresh_captcha(); + }); + $('form').validator().on('submit', function (e) { e.preventDefault(); var newPassword = $("#new_password ").val(); var password = $("#password").val(); + var captcha = $("#captcha").val(); $.ajax({ beforeSend: csrfTokenHeader, url: "/api/change_password/", - data: {new_password: newPassword, old_password: password}, + data: {new_password: newPassword, old_password: password, captcha: captcha}, dataType: "json", method: "post", success: function (data) { @@ -15,6 +24,7 @@ require(["jquery", "bsAlert", "csrfToken", "validator"], function ($, bsAlert, c window.location.href = "/login/"; } else { + refresh_captcha(); bsAlert(data.data); } } diff --git a/static/src/js/app/oj/account/login.js b/static/src/js/app/oj/account/login.js index 0a05762..32343c9 100644 --- a/static/src/js/app/oj/account/login.js +++ b/static/src/js/app/oj/account/login.js @@ -1,26 +1,31 @@ require(["jquery", "bsAlert", "csrfToken", "validator"], function ($, bsAlert, csrfTokenHeader) { + var applied_captcha = false; $('form').validator().on('submit', function (e) { if (!e.isDefaultPrevented()) { var username = $("#username").val(); var password = $("#password").val(); + var ajaxData = {username: username, password: password}; + if (applied_captcha) { + ajaxData.captcha = $("#captcha").val(); + } $.ajax({ beforeSend: csrfTokenHeader, url: "/api/login/", - data: {username: username, password: password}, + data: ajaxData, dataType: "json", method: "post", success: function (data) { if (!data.code) { //成功登陆 var ref = document.referrer; - if(ref){ + if (ref) { // 注册页和本页的来源的跳转回首页,防止死循环 - if(ref.indexOf("register") > -1 || ref.indexOf("login") > -1){ + if (ref.indexOf("register") > -1 || ref.indexOf("login") > -1) { location.href = "/"; return; } // 判断来源,只有同域下才跳转 - if(ref.split("/")[2].split(":")[0] == location.hostname){ + if (ref.split("/")[2].split(":")[0] == location.hostname) { location.href = ref; return; } @@ -28,6 +33,7 @@ require(["jquery", "bsAlert", "csrfToken", "validator"], function ($, bsAlert, c location.href = "/"; } else { + refresh_captcha(); bsAlert(data.data); } } @@ -35,5 +41,34 @@ require(["jquery", "bsAlert", "csrfToken", "validator"], function ($, bsAlert, c }); return false; } - }) + }); + + $('#username').blur(function () { + if ($("#username").val()) { + $.ajax({ + beforeSend: csrfTokenHeader, + url: "/api/account_security_check/?username=" + $("#username").val(), + method: "get", + success: function (data) { + if (!data.code) { + if (data.data.applied_captcha) { + $('#captcha-area').html('  

'); + applied_captcha = true; + } + else { + $('#captcha-area').html(''); + applied_captcha = false; + } + } + } + }); + } + }); + function refresh_captcha(){ + $("#captcha-img")[0].src = "/captcha/?" + Math.random(); + $("#captcha")[0].value = ""; + } + $("#captcha-img").click(function(){ + refresh_captcha(); + }); }); \ No newline at end of file diff --git a/static/src/js/app/oj/account/register.js b/static/src/js/app/oj/account/register.js index 9537fdb..e5d22d5 100644 --- a/static/src/js/app/oj/account/register.js +++ b/static/src/js/app/oj/account/register.js @@ -5,10 +5,11 @@ require(["jquery", "bsAlert", "csrfToken", "validator"], function ($, bsAlert, c var realName = $("#real_name").val(); var password = $("#password").val(); var email = $("#email").val(); + var captcha = $("#captcha").val(); $.ajax({ beforeSend: csrfTokenHeader, url: "/api/register/", - data: {username: username, real_name: realName, password: password, email: email}, + data: {username: username, real_name: realName, password: password, email: email, captcha:captcha}, dataType: "json", method: "post", success: function (data) { @@ -16,11 +17,20 @@ require(["jquery", "bsAlert", "csrfToken", "validator"], function ($, bsAlert, c window.location.href = "/login/"; } else { + refresh_captcha(); bsAlert(data.data); } } }); return false; } - }) + }); + function refresh_captcha() { + $("#captcha-img")[0].src = "/captcha/?" + Math.random(); + $("#captcha")[0].value = ""; + } + + $("#captcha-img").click(function () { + refresh_captcha(); + }); }); \ No newline at end of file diff --git a/static/src/js/app/oj/contest/contest_password.js b/static/src/js/app/oj/contest/contestPassword.js similarity index 100% rename from static/src/js/app/oj/contest/contest_password.js rename to static/src/js/app/oj/contest/contestPassword.js diff --git a/static/src/js/app/oj/problem/problem.js b/static/src/js/app/oj/problem/problem.js index 1faeb6d..b018f52 100644 --- a/static/src/js/app/oj/problem/problem.js +++ b/static/src/js/app/oj/problem/problem.js @@ -1,157 +1,252 @@ -require(["jquery", "codeMirror", "csrfToken", "bsAlert"], function ($, codeMirror, csrfTokenHeader, bsAlert) { - var codeEditor = codeMirror($("#code-editor")[0], "text/x-csrc"); - var language = $("input[name='language'][checked]").val(); - var submissionId; - - $("input[name='language']").change(function () { - language = this.value; - var languageTypes = {"1": "text/x-csrc", "2": "text/x-c++src", "3": "text/x-java"}; - codeEditor.setOption("mode", languageTypes[language]); - }); - - $("#show-more-btn").click(function () { - $(".hide").attr("class", "problem-section"); - $("#show-more-btn").hide(); - }); - - function showLoading() { - $("#submit-code-button").attr("disabled", "disabled"); - $("#loading-gif").show(); - } - - function hideLoading() { - $("#submit-code-button").removeAttr("disabled"); - $("#loading-gif").hide(); - } - - function getResultHtml(data) { - // 0 结果正确 1 运行错误 2 超时 3 超内存 4 编译错误 - // 5 格式错误 6 结果错误 7 系统错误 8 等待判题 - var results = { - 0: {"alert_class": "success", message: "Accepted"}, - 1: {"alert_class": "danger", message: "Runtime Error"}, - 2: {"alert_class": "warning", message: "Time Limit Exceeded"}, - 3: {"alert_class": "warning", message: "Memory Limit Exceeded"}, - 4: {"alert_class": "danger", message: "Compile Error"}, - 5: {"alert_class": "warning", message: "Format Error"}, - 6: {"alert_class": "danger", message: "Wrong Answer"}, - 7: {"alert_class": "danger", message: "System Error"}, - 8: {"alert_class": "info", message: "Waiting"} - }; - - var html = ''); - return html; - } + if(detect_flash()) { + // 提供点击复制到剪切板的功能 + ZeroClipboard.config({swfPath: "/static/img/ZeroClipboard.swf"}); + new ZeroClipboard($(".copy-sample")); + } + else{ + $(".copy-sample").hide(); + } - var counter = 0; - - function getResult() { - if (counter++ > 10) { - hideLoading(); - bsAlert("抱歉,服务器可能出现了故障,请稍后到我的提交列表中查看"); - counter = 0; + var codeEditorSelector = $("#code-editor")[0]; + // 部分界面逻辑会隐藏代码输入框,先判断有没有。 + if (codeEditorSelector == undefined) { return; } + + var codeEditor = codeMirror(codeEditorSelector, "text/x-csrc"); + var language = $("input[name='language'][checked]").val(); + var submissionId; + + $("input[name='language']").change(function () { + language = this.value; + var languageTypes = {"1": "text/x-csrc", "2": "text/x-c++src", "3": "text/x-java"}; + codeEditor.setOption("mode", languageTypes[language]); + }); + + $("#show-more-btn").click(function () { + $(".hide").attr("class", "problem-section"); + $("#show-more-btn").hide(); + }); + + function showLoading() { + $("#submit-code-button").attr("disabled", "disabled"); + $("#loading-gif").show(); + } + + function hideLoading() { + $("#submit-code-button").removeAttr("disabled"); + $("#loading-gif").hide(); + } + + function getResultHtml(data) { + // 0 结果正确 1 运行错误 2 超时 3 超内存 4 编译错误 + // 5 格式错误 6 结果错误 7 系统错误 8 等待判题 + var results = { + 0: {"alert_class": "success", message: "Accepted"}, + 1: {"alert_class": "danger", message: "Runtime Error"}, + 2: {"alert_class": "warning", message: "Time Limit Exceeded"}, + 3: {"alert_class": "warning", message: "Memory Limit Exceeded"}, + 4: {"alert_class": "danger", message: "Compile Error"}, + 5: {"alert_class": "warning", message: "Format Error"}, + 6: {"alert_class": "danger", message: "Wrong Answer"}, + 7: {"alert_class": "danger", message: "System Error"}, + 8: {"alert_class": "info", message: "Waiting"} + }; + + var html = ''); + + return html; + } + + var counter = 0; + + function getResult() { + if (counter++ > 10) { + hideLoading(); + bsAlert("抱歉,服务器正在紧张判题中,请稍后到我的提交列表中查看"); + counter = 0; + return; + } + $.ajax({ + url: "/api/submission/?submission_id=" + submissionId, + method: "get", + dataType: "json", + success: function (data) { + if (!data.code) { + // 8是还没有完成判题 + if (data.data.result == 8) { + // 1秒之后重新去获取 + setTimeout(getResult, 1000); + } + else { + counter = 0; + hideLoading(); + $("#result").html(getResultHtml(data.data)); + } + } + else { + bsAlert(data.data); + hideLoading(); + } + } + }) + } + + function guessLanguage(code) { + //cpp + if (code.indexOf("using namespace std") > -1||code.indexOf("") > -1) { + return "2"; + } + if (code.indexOf("printf") > -1) + { + return "1"; + } + //java + if (code.indexOf("public class Main") > -1) { + return "3"; + } + } + + function getServerTime(){ + var contestId = location.pathname.split("/")[2]; + var time = 0; + $.ajax({ + url: "/api/contest/time/?contest_id=" + contestId + "&type=end", + dataType: "json", + method: "get", + async: false, + success: function(data){ + if(!data.code){ + time = data.data; + } + }, + error: function(){ + time = new Date().getTime(); + } + }); + return time; + } + + if(location.href.indexOf("contest") > -1) { + setInterval(function () { + var time = getServerTime(); + var minutes = parseInt(time / (1000 * 60)); + if(minutes == 0){ + bsAlert("比赛即将结束"); + } + else if(minutes > 0 && minutes <= 5){ + bsAlert("比赛还剩" + minutes.toString() + "分钟"); + } + }, 1000 * 60); + } + + $("#submit-code-button").click(function () { + + var code = codeEditor.getValue(); + + if (!code.trim()) { + bsAlert("请填写代码!"); + hideLoading(); + return false; + } + + if (guessLanguage(code) != language) { + if (!confirm("您选择的代码语言可能存在错误,是否继续提交?")) { + return; + } + } + + if (language < 3) { + if (code.indexOf("__int64") > -1) { + if (!confirm("您是否在尝试使用'__int64'类型? 这不是 c/c++ 标准并将引发编译错误可以使用 'long long' 代替(详见 关于->帮助),是否仍然提交?")) { + return; + } + } + if (code.indexOf("%I64d") > -1) { + if (!confirm("您是否在尝试将'%I64d'用于long long类型的I/O? 这不被支持,并可能会导致程序输出异常,可以使用 '%lld' 代替(详见 关于->帮助),是否仍然提交?")) { + return; + } + } + } + + if (location.href.indexOf("contest") > -1) { + var problemId = location.pathname.split("/")[4]; + var contestId = location.pathname.split("/")[2]; + var url = "/api/contest/submission/"; + var data = { + problem_id: problemId, + language: language, + code: code, + contest_id: contestId + }; + } + else { + var problemId = window.location.pathname.split("/")[2]; + var url = "/api/submission/"; + var data = { + problem_id: problemId, + language: language, + code: code + }; + } + + showLoading(); + + $("#result").html(""); + + $.ajax({ + beforeSend: csrfTokenHeader, + url: url, + method: "post", + data: JSON.stringify(data), + contentType: "application/json", + success: function (data) { + if (!data.code) { + submissionId = data.data.submission_id; + // 获取到id 之后2秒去查询一下判题结果 + setTimeout(getResult, 2000); + } + else { + bsAlert(data.data); + hideLoading(); + } + } + }); + + }); + $.ajax({ - url: "/api/submission/?submission_id=" + submissionId, + url: "/api/user/", method: "get", dataType: "json", success: function (data) { - if (!data.code) { - // 8是还没有完成判题 - if (data.data.result == 8) { - // 1秒之后重新去获取 - setTimeout(getResult, 1000); - } - else { - counter = 0; - hideLoading(); - $("#result").html(getResultHtml(data.data)); - } - } - else { - bsAlert(data.data); - hideLoading(); + if (data.code) { + $("#submit-code-button").attr("disabled", "disabled"); + $("#result").html(''); } } }) - } - - $("#submit-code-button").click(function () { - - var code = codeEditor.getValue(); - if (location.href.indexOf("contest") > -1) { - var problemId = location.pathname.split("/")[4]; - var contestId = location.pathname.split("/")[2]; - var url = "/api/contest/submission/"; - var data = { - problem_id: problemId, - language: language, - code: code, - contest_id: contestId - }; - } - else { - var problemId = window.location.pathname.split("/")[2]; - var url = "/api/submission/"; - var data = { - problem_id: problemId, - language: language, - code: code - }; - } - - showLoading(); - - if (!code.trim()) { - bsAlert("请填写代码!"); - hideLoading(); - return false; - } - - $("#result").html(""); - - - $.ajax({ - beforeSend: csrfTokenHeader, - url: url, - method: "post", - data: JSON.stringify(data), - contentType: "application/json", - success: function (data) { - if (!data.code) { - submissionId = data.data.submission_id; - // 获取到id 之后2秒去查询一下判题结果 - setTimeout(getResult, 2000); - } - else { - bsAlert(data.data); - hideLoading(); - } - } - }); - - }); - - $.ajax({ - url: "/api/user/", - method: "get", - dataType: "json", - success: function (data) { - if (data.code) { - $("#submit-code-button").attr("disabled", "disabled"); - $("#result").html(''); - } - } - }) -}); + }); \ No newline at end of file diff --git a/static/src/js/app/oj/problem/problem_list.js b/static/src/js/app/oj/problem/problem_list.js deleted file mode 100644 index 2219946..0000000 --- a/static/src/js/app/oj/problem/problem_list.js +++ /dev/null @@ -1,6 +0,0 @@ -require(["jquery", "avalon"], function($, avalon){ - var vm = avalon.define({ - $id: "problem_list", - problem_list: [] - }) -}); \ No newline at end of file diff --git a/static/src/js/build.js b/static/src/js/build.js index 9927a1b..3acf938 100644 --- a/static/src/js/build.js +++ b/static/src/js/build.js @@ -1,10 +1,10 @@ ({ // RequireJS 通过一个相对的路径 baseUrl来加载所有代码。baseUrl通常被设置成data-main属性指定脚本的同级目录。 - baseUrl: "js/", + baseUrl: "./js", // 第三方脚本模块的别名,jquery比libs/jquery-1.11.1.min.js简洁明了; paths: { - jquery: "lib/jquery/jquery", - avalon: "lib/avalon/avalon", + jquery: "empty:", + avalon: "empty:", editor: "utils/editor", uploader: "utils/uploader", formValidation: "utils/formValidation", @@ -18,9 +18,9 @@ tagEditor: "lib/tagEditor/jquery.tag-editor.min", jqueryUI: "lib/jqueryUI/jquery-ui", bootstrap: "lib/bootstrap/bootstrap", - datetimePicker: "lib/datetime_picker/bootstrap-datetimepicker.zh-CN", + datetimePicker: "lib/datetime_picker/bootstrap-datetimepicker", validator: "lib/validator/validator", - + ZeroClipboard: "lib/ZeroClipboard/ZeroClipboard", // ------ 下面写的都不要直接用,而是使用上面的封装版本 ------ //富文本编辑器simditor -> editor @@ -33,35 +33,109 @@ _codeMirror: "lib/codeMirror/codemirror", codeMirrorClang: "lib/codeMirror/language/clike", + // bootstrap组件 + modal: "lib/bootstrap/modal", + dropdown: "lib/bootstrap/dropdown", + transition: "lib/bootstrap/transition", + //百度webuploader -> uploader webUploader: "lib/webuploader/webuploader", - "_datetimePicker": "lib/datetime_picker/bootstrap-datetimepicker" + //"_datetimePicker": "lib/datetime_picker/bootstrap-datetimepicker", - }, - shim: { - "bootstrap": {"deps": ['jquery']}, - "_datetimepicker": {"deps": ["jquery"]}, - "datetimepicker": {"deps": ["_datetimepicker"]} + //以下都是页面 script 标签引用的js + addProblem_0_pack: "app/admin/problem/addProblem", + addContest_1_pack: "app/admin/contest/addContest", + problem_2_pack: "app/admin/problem/problem", + register_3_pack: "app/oj/account/register", + contestList_4_pack: "app/admin/contest/contestList", + group_5_pack: "app/oj/group/group", + editProblem_6_pack: "app/admin/problem/editProblem", + announcement_7_pack: "app/admin/announcement/announcement", + monitor_8_pack: "app/admin/monitor/monitor", + groupDetail_9_pack: "app/admin/group/groupDetail", + admin_10_pack: "app/admin/admin", + problem_11_pack: "app/oj/problem/problem", + submissionList_12_pack: "app/admin/problem/submissionList", + editProblem_13_pack: "app/admin/contest/editProblem", + joinGroupRequestList_14_pack: "app/admin/group/joinGroupRequestList", + changePassword_15_pack: "app/oj/account/changePassword", + group_16_pack: "app/admin/group/group", + submissionList_17_pack: "app/admin/contest/submissionList", + login_18_pack: "app/oj/account/login", + contestPassword_19_pack: "app/oj/contest/contestPassword", + userList_20_pack: "app/admin/user/userList" }, findNestedDependencies: true, appDir: "../", dir: "../../release/", modules: [ { - name: "submit_code" + name: "bootstrap", }, { - name: "validation" + name: "addProblem_0_pack" }, { - name: "editor" + name: "addContest_1_pack" }, { - name: "code_mirror" + name: "problem_2_pack" }, { - name: "datetimepicker" + name: "register_3_pack" + }, + { + name: "contestList_4_pack" + }, + { + name: "group_5_pack" + }, + { + name: "editProblem_6_pack" + }, + { + name: "announcement_7_pack" + }, + { + name: "monitor_8_pack" + }, + { + name: "groupDetail_9_pack" + }, + { + name: "admin_10_pack" + }, + { + name: "problem_11_pack" + }, + { + name: "submissionList_12_pack" + }, + { + name: "editProblem_13_pack" + }, + { + name: "joinGroupRequestList_14_pack" + }, + { + name: "changePassword_15_pack" + }, + { + name: "group_16_pack" + }, + { + name: "submissionList_17_pack" + }, + { + name: "login_18_pack" + }, + { + name: "contestPassword_19_pack" + }, + { + name: "userList_20_pack" } - ] + ], + optimizeCss: "standard", }) \ No newline at end of file diff --git a/static/src/js/config.js b/static/src/js/config.js index ba68fd5..ce6e508 100644 --- a/static/src/js/config.js +++ b/static/src/js/config.js @@ -2,7 +2,6 @@ var require = { // RequireJS 通过一个相对的路径 baseUrl来加载所有代码。baseUrl通常被设置成data-main属性指定脚本的同级目录。 baseUrl: "/static/js/", paths: { - jquery: "lib/jquery/jquery", avalon: "lib/avalon/avalon", editor: "utils/editor", @@ -18,8 +17,9 @@ var require = { tagEditor: "lib/tagEditor/jquery.tag-editor.min", jqueryUI: "lib/jqueryUI/jquery-ui", bootstrap: "lib/bootstrap/bootstrap", - datetimePicker: "lib/datetime_picker/bootstrap-datetimepicker.zh-CN", + datetimePicker: "lib/datetime_picker/bootstrap-datetimepicker", validator: "lib/validator/validator", + ZeroClipboard: "lib/ZeroClipboard/ZeroClipboard", // ------ 下面写的都不要直接用,而是使用上面的封装版本 ------ @@ -33,15 +33,37 @@ var require = { _codeMirror: "lib/codeMirror/codemirror", codeMirrorClang: "lib/codeMirror/language/clike", + // bootstrap组件 + modal: "lib/bootstrap/modal", + dropdown: "lib/bootstrap/dropdown", + transition: "lib/bootstrap/transition", + //百度webuploader -> uploader webUploader: "lib/webuploader/webuploader", - "_datetimePicker": "lib/datetime_picker/bootstrap-datetimepicker" - }, - shim: { - bootstrap: {deps: ["jquery"]}, - _datetimePicker: {dep: ["jquery"]}, - datetimePicker: {deps: ["_datetimePicker"]}, - validator: ["jquery"] + // "_datetimePicker": "lib/datetime_picker/bootstrap-datetimepicker", + + //以下都是页面 script 标签引用的js + addProblem_0_pack: "app/admin/problem/addProblem", + addContest_1_pack: "app/admin/contest/addContest", + problem_2_pack: "app/admin/problem/problem", + register_3_pack: "app/oj/account/register", + contestList_4_pack: "app/admin/contest/contestList", + group_5_pack: "app/oj/group/group", + editProblem_6_pack: "app/admin/problem/editProblem", + announcement_7_pack: "app/admin/announcement/announcement", + monitor_8_pack: "app/admin/monitor/monitor", + groupDetail_9_pack: "app/admin/group/groupDetail", + admin_10_pack: "app/admin/admin", + problem_11_pack: "app/oj/problem/problem", + submissionList_12_pack: "app/admin/problem/submissionList", + editProblem_13_pack: "app/admin/contest/editProblem", + joinGroupRequestList_14_pack: "app/admin/group/joinGroupRequestList", + changePassword_15_pack: "app/oj/account/changePassword", + group_16_pack: "app/admin/group/group", + submissionList_17_pack: "app/admin/contest/submissionList", + login_18_pack: "app/oj/account/login", + contestPassword_19_pack: "app/oj/contest/contestPassword", + userList_20_pack: "app/admin/user/userList" } }; \ No newline at end of file diff --git a/static/src/js/lib/ZeroClipboard/ZeroClipboard.js b/static/src/js/lib/ZeroClipboard/ZeroClipboard.js new file mode 100755 index 0000000..63912cd --- /dev/null +++ b/static/src/js/lib/ZeroClipboard/ZeroClipboard.js @@ -0,0 +1,2610 @@ +/*! + * ZeroClipboard + * The ZeroClipboard library provides an easy way to copy text to the clipboard using an invisible Adobe Flash movie and a JavaScript interface. + * Copyright (c) 2009-2015 Jon Rohan, James M. Greene + * Licensed MIT + * http://zeroclipboard.org/ + * v2.3.0-beta.1 + */ +(function(window, undefined) { + "use strict"; + /** + * Store references to critically important global functions that may be + * overridden on certain web pages. + */ + var _window = window, _document = _window.document, _navigator = _window.navigator, _setTimeout = _window.setTimeout, _clearTimeout = _window.clearTimeout, _setInterval = _window.setInterval, _clearInterval = _window.clearInterval, _getComputedStyle = _window.getComputedStyle, _encodeURIComponent = _window.encodeURIComponent, _ActiveXObject = _window.ActiveXObject, _Error = _window.Error, _parseInt = _window.Number.parseInt || _window.parseInt, _parseFloat = _window.Number.parseFloat || _window.parseFloat, _isNaN = _window.Number.isNaN || _window.isNaN, _now = _window.Date.now, _keys = _window.Object.keys, _defineProperty = _window.Object.defineProperty, _hasOwn = _window.Object.prototype.hasOwnProperty, _slice = _window.Array.prototype.slice, _unwrap = function() { + var unwrapper = function(el) { + return el; + }; + if (typeof _window.wrap === "function" && typeof _window.unwrap === "function") { + try { + var div = _document.createElement("div"); + var unwrappedDiv = _window.unwrap(div); + if (div.nodeType === 1 && unwrappedDiv && unwrappedDiv.nodeType === 1) { + unwrapper = _window.unwrap; + } + } catch (e) {} + } + return unwrapper; + }(); + /** + * Convert an `arguments` object into an Array. + * + * @returns The arguments as an Array + * @private + */ + var _args = function(argumentsObj) { + return _slice.call(argumentsObj, 0); + }; + /** + * Shallow-copy the owned, enumerable properties of one object over to another, similar to jQuery's `$.extend`. + * + * @returns The target object, augmented + * @private + */ + var _extend = function() { + var i, len, arg, prop, src, copy, args = _args(arguments), target = args[0] || {}; + for (i = 1, len = args.length; i < len; i++) { + if ((arg = args[i]) != null) { + for (prop in arg) { + if (_hasOwn.call(arg, prop)) { + src = target[prop]; + copy = arg[prop]; + if (target !== copy && copy !== undefined) { + target[prop] = copy; + } + } + } + } + } + return target; + }; + /** + * Return a deep copy of the source object or array. + * + * @returns Object or Array + * @private + */ + var _deepCopy = function(source) { + var copy, i, len, prop; + if (typeof source !== "object" || source == null || typeof source.nodeType === "number") { + copy = source; + } else if (typeof source.length === "number") { + copy = []; + for (i = 0, len = source.length; i < len; i++) { + if (_hasOwn.call(source, i)) { + copy[i] = _deepCopy(source[i]); + } + } + } else { + copy = {}; + for (prop in source) { + if (_hasOwn.call(source, prop)) { + copy[prop] = _deepCopy(source[prop]); + } + } + } + return copy; + }; + /** + * Makes a shallow copy of `obj` (like `_extend`) but filters its properties based on a list of `keys` to keep. + * The inverse of `_omit`, mostly. The big difference is that these properties do NOT need to be enumerable to + * be kept. + * + * @returns A new filtered object. + * @private + */ + var _pick = function(obj, keys) { + var newObj = {}; + for (var i = 0, len = keys.length; i < len; i++) { + if (keys[i] in obj) { + newObj[keys[i]] = obj[keys[i]]; + } + } + return newObj; + }; + /** + * Makes a shallow copy of `obj` (like `_extend`) but filters its properties based on a list of `keys` to omit. + * The inverse of `_pick`. + * + * @returns A new filtered object. + * @private + */ + var _omit = function(obj, keys) { + var newObj = {}; + for (var prop in obj) { + if (keys.indexOf(prop) === -1) { + newObj[prop] = obj[prop]; + } + } + return newObj; + }; + /** + * Remove all owned, enumerable properties from an object. + * + * @returns The original object without its owned, enumerable properties. + * @private + */ + var _deleteOwnProperties = function(obj) { + if (obj) { + for (var prop in obj) { + if (_hasOwn.call(obj, prop)) { + delete obj[prop]; + } + } + } + return obj; + }; + /** + * Determine if an element is contained within another element. + * + * @returns Boolean + * @private + */ + var _containedBy = function(el, ancestorEl) { + if (el && el.nodeType === 1 && el.ownerDocument && ancestorEl && (ancestorEl.nodeType === 1 && ancestorEl.ownerDocument && ancestorEl.ownerDocument === el.ownerDocument || ancestorEl.nodeType === 9 && !ancestorEl.ownerDocument && ancestorEl === el.ownerDocument)) { + do { + if (el === ancestorEl) { + return true; + } + el = el.parentNode; + } while (el); + } + return false; + }; + /** + * Get the URL path's parent directory. + * + * @returns String or `undefined` + * @private + */ + var _getDirPathOfUrl = function(url) { + var dir; + if (typeof url === "string" && url) { + dir = url.split("#")[0].split("?")[0]; + dir = url.slice(0, url.lastIndexOf("/") + 1); + } + return dir; + }; + /** + * Get the current script's URL by throwing an `Error` and analyzing it. + * + * @returns String or `undefined` + * @private + */ + var _getCurrentScriptUrlFromErrorStack = function(stack) { + var url, matches; + if (typeof stack === "string" && stack) { + matches = stack.match(/^(?:|[^:@]*@|.+\)@(?=http[s]?|file)|.+?\s+(?: at |@)(?:[^:\(]+ )*[\(]?)((?:http[s]?|file):\/\/[\/]?.+?\/[^:\)]*?)(?::\d+)(?::\d+)?/); + if (matches && matches[1]) { + url = matches[1]; + } else { + matches = stack.match(/\)@((?:http[s]?|file):\/\/[\/]?.+?\/[^:\)]*?)(?::\d+)(?::\d+)?/); + if (matches && matches[1]) { + url = matches[1]; + } + } + } + return url; + }; + /** + * Get the current script's URL by throwing an `Error` and analyzing it. + * + * @returns String or `undefined` + * @private + */ + var _getCurrentScriptUrlFromError = function() { + var url, err; + try { + throw new _Error(); + } catch (e) { + err = e; + } + if (err) { + url = err.sourceURL || err.fileName || _getCurrentScriptUrlFromErrorStack(err.stack); + } + return url; + }; + /** + * Get the current script's URL. + * + * @returns String or `undefined` + * @private + */ + var _getCurrentScriptUrl = function() { + var jsPath, scripts, i; + if (_document.currentScript && (jsPath = _document.currentScript.src)) { + return jsPath; + } + scripts = _document.getElementsByTagName("script"); + if (scripts.length === 1) { + return scripts[0].src || undefined; + } + if ("readyState" in scripts[0]) { + for (i = scripts.length; i--; ) { + if (scripts[i].readyState === "interactive" && (jsPath = scripts[i].src)) { + return jsPath; + } + } + } + if (_document.readyState === "loading" && (jsPath = scripts[scripts.length - 1].src)) { + return jsPath; + } + if (jsPath = _getCurrentScriptUrlFromError()) { + return jsPath; + } + return undefined; + }; + /** + * Get the unanimous parent directory of ALL script tags. + * If any script tags are either (a) inline or (b) from differing parent + * directories, this method must return `undefined`. + * + * @returns String or `undefined` + * @private + */ + var _getUnanimousScriptParentDir = function() { + var i, jsDir, jsPath, scripts = _document.getElementsByTagName("script"); + for (i = scripts.length; i--; ) { + if (!(jsPath = scripts[i].src)) { + jsDir = null; + break; + } + jsPath = _getDirPathOfUrl(jsPath); + if (jsDir == null) { + jsDir = jsPath; + } else if (jsDir !== jsPath) { + jsDir = null; + break; + } + } + return jsDir || undefined; + }; + /** + * Get the presumed location of the "ZeroClipboard.swf" file, based on the location + * of the executing JavaScript file (e.g. "ZeroClipboard.js", etc.). + * + * @returns String + * @private + */ + var _getDefaultSwfPath = function() { + var jsDir = _getDirPathOfUrl(_getCurrentScriptUrl()) || _getUnanimousScriptParentDir() || ""; + return jsDir + "ZeroClipboard.swf"; + }; + /** + * Is the client's operating system some version of Windows? + * + * @returns Boolean + * @private + */ + var _isWindows = function() { + var isWindowsRegex = /win(dows|[\s]?(nt|me|ce|xp|vista|[\d]+))/i; + return !!_navigator && (isWindowsRegex.test(_navigator.appVersion || "") || isWindowsRegex.test(_navigator.platform || "") || (_navigator.userAgent || "").indexOf("Windows") !== -1); + }; + /** + * Keep track of if the page is framed (in an `iframe`). This can never change. + * @private + */ + var _pageIsFramed = function() { + return window.opener == null && (!!window.top && window != window.top || !!window.parent && window != window.parent); + }(); + /** + * Keep track of the state of the Flash object. + * @private + */ + var _flashState = { + bridge: null, + version: "0.0.0", + pluginType: "unknown", + disabled: null, + outdated: null, + sandboxed: null, + unavailable: null, + degraded: null, + deactivated: null, + overdue: null, + ready: null + }; + /** + * The minimum Flash Player version required to use ZeroClipboard completely. + * @readonly + * @private + */ + var _minimumFlashVersion = "11.0.0"; + /** + * The ZeroClipboard library version number, as reported by Flash, at the time the SWF was compiled. + */ + var _zcSwfVersion; + /** + * Keep track of all event listener registrations. + * @private + */ + var _handlers = {}; + /** + * Keep track of the currently activated element. + * @private + */ + var _currentElement; + /** + * Keep track of the element that was activated when a `copy` process started. + * @private + */ + var _copyTarget; + /** + * Keep track of data for the pending clipboard transaction. + * @private + */ + var _clipData = {}; + /** + * Keep track of data formats for the pending clipboard transaction. + * @private + */ + var _clipDataFormatMap = null; + /** + * Keep track of the Flash availability check timeout. + * @private + */ + var _flashCheckTimeout = 0; + /** + * Keep track of SWF network errors interval polling. + * @private + */ + var _swfFallbackCheckInterval = 0; + /** + * The `message` store for events + * @private + */ + var _eventMessages = { + ready: "Flash communication is established", + error: { + "flash-disabled": "Flash is disabled or not installed. May also be attempting to run Flash in a sandboxed iframe, which is impossible.", + "flash-outdated": "Flash is too outdated to support ZeroClipboard", + "flash-sandboxed": "Attempting to run Flash in a sandboxed iframe, which is impossible", + "flash-unavailable": "Flash is unable to communicate bidirectionally with JavaScript", + "flash-degraded": "Flash is unable to preserve data fidelity when communicating with JavaScript", + "flash-deactivated": "Flash is too outdated for your browser and/or is configured as click-to-activate.\nThis may also mean that the ZeroClipboard SWF object could not be loaded, so please check your `swfPath` configuration and/or network connectivity.\nMay also be attempting to run Flash in a sandboxed iframe, which is impossible.", + "flash-overdue": "Flash communication was established but NOT within the acceptable time limit", + "version-mismatch": "ZeroClipboard JS version number does not match ZeroClipboard SWF version number", + "clipboard-error": "At least one error was thrown while ZeroClipboard was attempting to inject your data into the clipboard", + "config-mismatch": "ZeroClipboard configuration does not match Flash's reality", + "swf-not-found": "The ZeroClipboard SWF object could not be loaded, so please check your `swfPath` configuration and/or network connectivity" + } + }; + /** + * The `name`s of `error` events that can only occur is Flash has at least + * been able to load the SWF successfully. + * @private + */ + var _errorsThatOnlyOccurAfterFlashLoads = [ "flash-unavailable", "flash-degraded", "flash-overdue", "version-mismatch", "config-mismatch", "clipboard-error" ]; + /** + * The `name`s of `error` events that should likely result in the `_flashState` + * variable's property values being updated. + * @private + */ + var _flashStateErrorNames = [ "flash-disabled", "flash-outdated", "flash-sandboxed", "flash-unavailable", "flash-degraded", "flash-deactivated", "flash-overdue" ]; + /** + * A RegExp to match the `name` property of `error` events related to Flash. + * @private + */ + var _flashStateErrorNameMatchingRegex = new RegExp("^flash-(" + _flashStateErrorNames.map(function(errorName) { + return errorName.replace(/^flash-/, ""); + }).join("|") + ")$"); + /** + * A RegExp to match the `name` property of `error` events related to Flash, + * which is enabled. + * @private + */ + var _flashStateEnabledErrorNameMatchingRegex = new RegExp("^flash-(" + _flashStateErrorNames.slice(1).map(function(errorName) { + return errorName.replace(/^flash-/, ""); + }).join("|") + ")$"); + /** + * ZeroClipboard configuration defaults for the Core module. + * @private + */ + var _globalConfig = { + swfPath: _getDefaultSwfPath(), + trustedDomains: window.location.host ? [ window.location.host ] : [], + cacheBust: true, + forceEnhancedClipboard: false, + flashLoadTimeout: 3e4, + autoActivate: true, + bubbleEvents: true, + fixLineEndings: true, + containerId: "global-zeroclipboard-html-bridge", + containerClass: "global-zeroclipboard-container", + swfObjectId: "global-zeroclipboard-flash-bridge", + hoverClass: "zeroclipboard-is-hover", + activeClass: "zeroclipboard-is-active", + forceHandCursor: false, + title: null, + zIndex: 999999999 + }; + /** + * The underlying implementation of `ZeroClipboard.config`. + * @private + */ + var _config = function(options) { + if (typeof options === "object" && options !== null) { + for (var prop in options) { + if (_hasOwn.call(options, prop)) { + if (/^(?:forceHandCursor|title|zIndex|bubbleEvents|fixLineEndings)$/.test(prop)) { + _globalConfig[prop] = options[prop]; + } else if (_flashState.bridge == null) { + if (prop === "containerId" || prop === "swfObjectId") { + if (_isValidHtml4Id(options[prop])) { + _globalConfig[prop] = options[prop]; + } else { + throw new Error("The specified `" + prop + "` value is not valid as an HTML4 Element ID"); + } + } else { + _globalConfig[prop] = options[prop]; + } + } + } + } + } + if (typeof options === "string" && options) { + if (_hasOwn.call(_globalConfig, options)) { + return _globalConfig[options]; + } + return; + } + return _deepCopy(_globalConfig); + }; + /** + * The underlying implementation of `ZeroClipboard.state`. + * @private + */ + var _state = function() { + _detectSandbox(); + return { + browser: _pick(_navigator, [ "userAgent", "platform", "appName", "appVersion" ]), + flash: _omit(_flashState, [ "bridge" ]), + zeroclipboard: { + version: ZeroClipboard.version, + config: ZeroClipboard.config() + } + }; + }; + /** + * The underlying implementation of `ZeroClipboard.isFlashUnusable`. + * @private + */ + var _isFlashUnusable = function() { + return !!(_flashState.disabled || _flashState.outdated || _flashState.sandboxed || _flashState.unavailable || _flashState.degraded || _flashState.deactivated); + }; + /** + * The underlying implementation of `ZeroClipboard.on`. + * @private + */ + var _on = function(eventType, listener) { + var i, len, events, added = {}; + if (typeof eventType === "string" && eventType) { + events = eventType.toLowerCase().split(/\s+/); + } else if (typeof eventType === "object" && eventType && typeof listener === "undefined") { + for (i in eventType) { + if (_hasOwn.call(eventType, i) && typeof i === "string" && i && typeof eventType[i] === "function") { + ZeroClipboard.on(i, eventType[i]); + } + } + } + if (events && events.length) { + for (i = 0, len = events.length; i < len; i++) { + eventType = events[i].replace(/^on/, ""); + added[eventType] = true; + if (!_handlers[eventType]) { + _handlers[eventType] = []; + } + _handlers[eventType].push(listener); + } + if (added.ready && _flashState.ready) { + ZeroClipboard.emit({ + type: "ready" + }); + } + if (added.error) { + for (i = 0, len = _flashStateErrorNames.length; i < len; i++) { + if (_flashState[_flashStateErrorNames[i].replace(/^flash-/, "")] === true) { + ZeroClipboard.emit({ + type: "error", + name: _flashStateErrorNames[i] + }); + break; + } + } + if (_zcSwfVersion !== undefined && ZeroClipboard.version !== _zcSwfVersion) { + ZeroClipboard.emit({ + type: "error", + name: "version-mismatch", + jsVersion: ZeroClipboard.version, + swfVersion: _zcSwfVersion + }); + } + } + } + return ZeroClipboard; + }; + /** + * The underlying implementation of `ZeroClipboard.off`. + * @private + */ + var _off = function(eventType, listener) { + var i, len, foundIndex, events, perEventHandlers; + if (arguments.length === 0) { + events = _keys(_handlers); + } else if (typeof eventType === "string" && eventType) { + events = eventType.split(/\s+/); + } else if (typeof eventType === "object" && eventType && typeof listener === "undefined") { + for (i in eventType) { + if (_hasOwn.call(eventType, i) && typeof i === "string" && i && typeof eventType[i] === "function") { + ZeroClipboard.off(i, eventType[i]); + } + } + } + if (events && events.length) { + for (i = 0, len = events.length; i < len; i++) { + eventType = events[i].toLowerCase().replace(/^on/, ""); + perEventHandlers = _handlers[eventType]; + if (perEventHandlers && perEventHandlers.length) { + if (listener) { + foundIndex = perEventHandlers.indexOf(listener); + while (foundIndex !== -1) { + perEventHandlers.splice(foundIndex, 1); + foundIndex = perEventHandlers.indexOf(listener, foundIndex); + } + } else { + perEventHandlers.length = 0; + } + } + } + } + return ZeroClipboard; + }; + /** + * The underlying implementation of `ZeroClipboard.handlers`. + * @private + */ + var _listeners = function(eventType) { + var copy; + if (typeof eventType === "string" && eventType) { + copy = _deepCopy(_handlers[eventType]) || null; + } else { + copy = _deepCopy(_handlers); + } + return copy; + }; + /** + * The underlying implementation of `ZeroClipboard.emit`. + * @private + */ + var _emit = function(event) { + var eventCopy, returnVal, tmp; + event = _createEvent(event); + if (!event) { + return; + } + if (_preprocessEvent(event)) { + return; + } + if (event.type === "ready" && _flashState.overdue === true) { + return ZeroClipboard.emit({ + type: "error", + name: "flash-overdue" + }); + } + eventCopy = _extend({}, event); + _dispatchCallbacks.call(this, eventCopy); + if (event.type === "copy") { + tmp = _mapClipDataToFlash(_clipData); + returnVal = tmp.data; + _clipDataFormatMap = tmp.formatMap; + } + return returnVal; + }; + /** + * The underlying implementation of `ZeroClipboard.create`. + * @private + */ + var _create = function() { + var previousState = _flashState.sandboxed; + _detectSandbox(); + if (typeof _flashState.ready !== "boolean") { + _flashState.ready = false; + } + if (_flashState.sandboxed !== previousState && _flashState.sandboxed === true) { + _flashState.ready = false; + ZeroClipboard.emit({ + type: "error", + name: "flash-sandboxed" + }); + } else if (!ZeroClipboard.isFlashUnusable() && _flashState.bridge === null) { + var maxWait = _globalConfig.flashLoadTimeout; + if (typeof maxWait === "number" && maxWait >= 0) { + _flashCheckTimeout = _setTimeout(function() { + if (typeof _flashState.deactivated !== "boolean") { + _flashState.deactivated = true; + } + if (_flashState.deactivated === true) { + ZeroClipboard.emit({ + type: "error", + name: "flash-deactivated" + }); + } + }, maxWait); + } + _flashState.overdue = false; + _embedSwf(); + } + }; + /** + * The underlying implementation of `ZeroClipboard.destroy`. + * @private + */ + var _destroy = function() { + ZeroClipboard.clearData(); + ZeroClipboard.blur(); + ZeroClipboard.emit("destroy"); + _unembedSwf(); + ZeroClipboard.off(); + }; + /** + * The underlying implementation of `ZeroClipboard.setData`. + * @private + */ + var _setData = function(format, data) { + var dataObj; + if (typeof format === "object" && format && typeof data === "undefined") { + dataObj = format; + ZeroClipboard.clearData(); + } else if (typeof format === "string" && format) { + dataObj = {}; + dataObj[format] = data; + } else { + return; + } + for (var dataFormat in dataObj) { + if (typeof dataFormat === "string" && dataFormat && _hasOwn.call(dataObj, dataFormat) && typeof dataObj[dataFormat] === "string" && dataObj[dataFormat]) { + _clipData[dataFormat] = _fixLineEndings(dataObj[dataFormat]); + } + } + }; + /** + * The underlying implementation of `ZeroClipboard.clearData`. + * @private + */ + var _clearData = function(format) { + if (typeof format === "undefined") { + _deleteOwnProperties(_clipData); + _clipDataFormatMap = null; + } else if (typeof format === "string" && _hasOwn.call(_clipData, format)) { + delete _clipData[format]; + } + }; + /** + * The underlying implementation of `ZeroClipboard.getData`. + * @private + */ + var _getData = function(format) { + if (typeof format === "undefined") { + return _deepCopy(_clipData); + } else if (typeof format === "string" && _hasOwn.call(_clipData, format)) { + return _clipData[format]; + } + }; + /** + * The underlying implementation of `ZeroClipboard.focus`/`ZeroClipboard.activate`. + * @private + */ + var _focus = function(element) { + if (!(element && element.nodeType === 1)) { + return; + } + if (_currentElement) { + _removeClass(_currentElement, _globalConfig.activeClass); + if (_currentElement !== element) { + _removeClass(_currentElement, _globalConfig.hoverClass); + } + } + _currentElement = element; + _addClass(element, _globalConfig.hoverClass); + var newTitle = element.getAttribute("title") || _globalConfig.title; + if (typeof newTitle === "string" && newTitle) { + var htmlBridge = _getHtmlBridge(_flashState.bridge); + if (htmlBridge) { + htmlBridge.setAttribute("title", newTitle); + } + } + var useHandCursor = _globalConfig.forceHandCursor === true || _getStyle(element, "cursor") === "pointer"; + _setHandCursor(useHandCursor); + _reposition(); + }; + /** + * The underlying implementation of `ZeroClipboard.blur`/`ZeroClipboard.deactivate`. + * @private + */ + var _blur = function() { + var htmlBridge = _getHtmlBridge(_flashState.bridge); + if (htmlBridge) { + htmlBridge.removeAttribute("title"); + htmlBridge.style.left = "0px"; + htmlBridge.style.top = "-9999px"; + htmlBridge.style.width = "1px"; + htmlBridge.style.height = "1px"; + } + if (_currentElement) { + _removeClass(_currentElement, _globalConfig.hoverClass); + _removeClass(_currentElement, _globalConfig.activeClass); + _currentElement = null; + } + }; + /** + * The underlying implementation of `ZeroClipboard.activeElement`. + * @private + */ + var _activeElement = function() { + return _currentElement || null; + }; + /** + * Check if a value is a valid HTML4 `ID` or `Name` token. + * @private + */ + var _isValidHtml4Id = function(id) { + return typeof id === "string" && id && /^[A-Za-z][A-Za-z0-9_:\-\.]*$/.test(id); + }; + /** + * Create or update an `event` object, based on the `eventType`. + * @private + */ + var _createEvent = function(event) { + var eventType; + if (typeof event === "string" && event) { + eventType = event; + event = {}; + } else if (typeof event === "object" && event && typeof event.type === "string" && event.type) { + eventType = event.type; + } + if (!eventType) { + return; + } + eventType = eventType.toLowerCase(); + if (!event.target && (/^(copy|aftercopy|_click)$/.test(eventType) || eventType === "error" && event.name === "clipboard-error")) { + event.target = _copyTarget; + } + _extend(event, { + type: eventType, + target: event.target || _currentElement || null, + relatedTarget: event.relatedTarget || null, + currentTarget: _flashState && _flashState.bridge || null, + timeStamp: event.timeStamp || _now() || null + }); + var msg = _eventMessages[event.type]; + if (event.type === "error" && event.name && msg) { + msg = msg[event.name]; + } + if (msg) { + event.message = msg; + } + if (event.type === "ready") { + _extend(event, { + target: null, + version: _flashState.version + }); + } + if (event.type === "error") { + if (_flashStateErrorNameMatchingRegex.test(event.name)) { + _extend(event, { + target: null, + minimumVersion: _minimumFlashVersion + }); + } + if (_flashStateEnabledErrorNameMatchingRegex.test(event.name)) { + _extend(event, { + version: _flashState.version + }); + } + } + if (event.type === "copy") { + event.clipboardData = { + setData: ZeroClipboard.setData, + clearData: ZeroClipboard.clearData + }; + } + if (event.type === "aftercopy") { + event = _mapClipResultsFromFlash(event, _clipDataFormatMap); + } + if (event.target && !event.relatedTarget) { + event.relatedTarget = _getRelatedTarget(event.target); + } + return _addMouseData(event); + }; + /** + * Get a relatedTarget from the target's `data-clipboard-target` attribute + * @private + */ + var _getRelatedTarget = function(targetEl) { + var relatedTargetId = targetEl && targetEl.getAttribute && targetEl.getAttribute("data-clipboard-target"); + return relatedTargetId ? _document.getElementById(relatedTargetId) : null; + }; + /** + * Add element and position data to `MouseEvent` instances + * @private + */ + var _addMouseData = function(event) { + if (event && /^_(?:click|mouse(?:over|out|down|up|move))$/.test(event.type)) { + var srcElement = event.target; + var fromElement = event.type === "_mouseover" && event.relatedTarget ? event.relatedTarget : undefined; + var toElement = event.type === "_mouseout" && event.relatedTarget ? event.relatedTarget : undefined; + var pos = _getElementPosition(srcElement); + var screenLeft = _window.screenLeft || _window.screenX || 0; + var screenTop = _window.screenTop || _window.screenY || 0; + var scrollLeft = _document.body.scrollLeft + _document.documentElement.scrollLeft; + var scrollTop = _document.body.scrollTop + _document.documentElement.scrollTop; + var pageX = pos.left + (typeof event._stageX === "number" ? event._stageX : 0); + var pageY = pos.top + (typeof event._stageY === "number" ? event._stageY : 0); + var clientX = pageX - scrollLeft; + var clientY = pageY - scrollTop; + var screenX = screenLeft + clientX; + var screenY = screenTop + clientY; + var moveX = typeof event.movementX === "number" ? event.movementX : 0; + var moveY = typeof event.movementY === "number" ? event.movementY : 0; + delete event._stageX; + delete event._stageY; + _extend(event, { + srcElement: srcElement, + fromElement: fromElement, + toElement: toElement, + screenX: screenX, + screenY: screenY, + pageX: pageX, + pageY: pageY, + clientX: clientX, + clientY: clientY, + x: clientX, + y: clientY, + movementX: moveX, + movementY: moveY, + offsetX: 0, + offsetY: 0, + layerX: 0, + layerY: 0 + }); + } + return event; + }; + /** + * Determine if an event's registered handlers should be execute synchronously or asynchronously. + * + * @returns {boolean} + * @private + */ + var _shouldPerformAsync = function(event) { + var eventType = event && typeof event.type === "string" && event.type || ""; + return !/^(?:(?:before)?copy|destroy)$/.test(eventType); + }; + /** + * Control if a callback should be executed asynchronously or not. + * + * @returns `undefined` + * @private + */ + var _dispatchCallback = function(func, context, args, async) { + if (async) { + _setTimeout(function() { + func.apply(context, args); + }, 0); + } else { + func.apply(context, args); + } + }; + /** + * Handle the actual dispatching of events to client instances. + * + * @returns `undefined` + * @private + */ + var _dispatchCallbacks = function(event) { + if (!(typeof event === "object" && event && event.type)) { + return; + } + var async = _shouldPerformAsync(event); + var wildcardTypeHandlers = _handlers["*"] || []; + var specificTypeHandlers = _handlers[event.type] || []; + var handlers = wildcardTypeHandlers.concat(specificTypeHandlers); + if (handlers && handlers.length) { + var i, len, func, context, eventCopy, originalContext = this; + for (i = 0, len = handlers.length; i < len; i++) { + func = handlers[i]; + context = originalContext; + if (typeof func === "string" && typeof _window[func] === "function") { + func = _window[func]; + } + if (typeof func === "object" && func && typeof func.handleEvent === "function") { + context = func; + func = func.handleEvent; + } + if (typeof func === "function") { + eventCopy = _extend({}, event); + _dispatchCallback(func, context, [ eventCopy ], async); + } + } + } + return this; + }; + /** + * Check an `error` event's `name` property to see if Flash has + * already loaded, which rules out possible `iframe` sandboxing. + * @private + */ + var _getSandboxStatusFromErrorEvent = function(event) { + var isSandboxed = null; + if (_pageIsFramed === false || event && event.type === "error" && event.name && _errorsThatOnlyOccurAfterFlashLoads.indexOf(event.name) !== -1) { + isSandboxed = false; + } + return isSandboxed; + }; + /** + * Preprocess any special behaviors, reactions, or state changes after receiving this event. + * Executes only once per event emitted, NOT once per client. + * @private + */ + var _preprocessEvent = function(event) { + var element = event.target || _currentElement || null; + var sourceIsSwf = event._source === "swf"; + delete event._source; + switch (event.type) { + case "error": + var isSandboxed = event.name === "flash-sandboxed" || _getSandboxStatusFromErrorEvent(event); + if (typeof isSandboxed === "boolean") { + _flashState.sandboxed = isSandboxed; + } + if (_flashStateErrorNames.indexOf(event.name) !== -1) { + _extend(_flashState, { + disabled: event.name === "flash-disabled", + outdated: event.name === "flash-outdated", + unavailable: event.name === "flash-unavailable", + degraded: event.name === "flash-degraded", + deactivated: event.name === "flash-deactivated", + overdue: event.name === "flash-overdue", + ready: false + }); + } else if (event.name === "version-mismatch") { + _zcSwfVersion = event.swfVersion; + _extend(_flashState, { + disabled: false, + outdated: false, + unavailable: false, + degraded: false, + deactivated: false, + overdue: false, + ready: false + }); + } + _clearTimeoutsAndPolling(); + break; + + case "ready": + _zcSwfVersion = event.swfVersion; + var wasDeactivated = _flashState.deactivated === true; + _extend(_flashState, { + disabled: false, + outdated: false, + sandboxed: false, + unavailable: false, + degraded: false, + deactivated: false, + overdue: wasDeactivated, + ready: !wasDeactivated + }); + _clearTimeoutsAndPolling(); + break; + + case "beforecopy": + _copyTarget = element; + break; + + case "copy": + var textContent, htmlContent, targetEl = event.relatedTarget; + if (!(_clipData["text/html"] || _clipData["text/plain"]) && targetEl && (htmlContent = targetEl.value || targetEl.outerHTML || targetEl.innerHTML) && (textContent = targetEl.value || targetEl.textContent || targetEl.innerText)) { + event.clipboardData.clearData(); + event.clipboardData.setData("text/plain", textContent); + if (htmlContent !== textContent) { + event.clipboardData.setData("text/html", htmlContent); + } + } else if (!_clipData["text/plain"] && event.target && (textContent = event.target.getAttribute("data-clipboard-text"))) { + event.clipboardData.clearData(); + event.clipboardData.setData("text/plain", textContent); + } + break; + + case "aftercopy": + _queueEmitClipboardErrors(event); + ZeroClipboard.clearData(); + if (element && element !== _safeActiveElement() && element.focus) { + element.focus(); + } + break; + + case "_mouseover": + ZeroClipboard.focus(element); + if (_globalConfig.bubbleEvents === true && sourceIsSwf) { + if (element && element !== event.relatedTarget && !_containedBy(event.relatedTarget, element)) { + _fireMouseEvent(_extend({}, event, { + type: "mouseenter", + bubbles: false, + cancelable: false + })); + } + _fireMouseEvent(_extend({}, event, { + type: "mouseover" + })); + } + break; + + case "_mouseout": + ZeroClipboard.blur(); + if (_globalConfig.bubbleEvents === true && sourceIsSwf) { + if (element && element !== event.relatedTarget && !_containedBy(event.relatedTarget, element)) { + _fireMouseEvent(_extend({}, event, { + type: "mouseleave", + bubbles: false, + cancelable: false + })); + } + _fireMouseEvent(_extend({}, event, { + type: "mouseout" + })); + } + break; + + case "_mousedown": + _addClass(element, _globalConfig.activeClass); + if (_globalConfig.bubbleEvents === true && sourceIsSwf) { + _fireMouseEvent(_extend({}, event, { + type: event.type.slice(1) + })); + } + break; + + case "_mouseup": + _removeClass(element, _globalConfig.activeClass); + if (_globalConfig.bubbleEvents === true && sourceIsSwf) { + _fireMouseEvent(_extend({}, event, { + type: event.type.slice(1) + })); + } + break; + + case "_click": + _copyTarget = null; + if (_globalConfig.bubbleEvents === true && sourceIsSwf) { + _fireMouseEvent(_extend({}, event, { + type: event.type.slice(1) + })); + } + break; + + case "_mousemove": + if (_globalConfig.bubbleEvents === true && sourceIsSwf) { + _fireMouseEvent(_extend({}, event, { + type: event.type.slice(1) + })); + } + break; + } + if (/^_(?:click|mouse(?:over|out|down|up|move))$/.test(event.type)) { + return true; + } + }; + /** + * Check an "aftercopy" event for clipboard errors and emit a corresponding "error" event. + * @private + */ + var _queueEmitClipboardErrors = function(aftercopyEvent) { + if (aftercopyEvent.errors && aftercopyEvent.errors.length > 0) { + var errorEvent = _deepCopy(aftercopyEvent); + _extend(errorEvent, { + type: "error", + name: "clipboard-error" + }); + delete errorEvent.success; + _setTimeout(function() { + ZeroClipboard.emit(errorEvent); + }, 0); + } + }; + /** + * Dispatch a synthetic MouseEvent. + * + * @returns `undefined` + * @private + */ + var _fireMouseEvent = function(event) { + if (!(event && typeof event.type === "string" && event)) { + return; + } + var e, target = event.target || null, doc = target && target.ownerDocument || _document, defaults = { + view: doc.defaultView || _window, + canBubble: true, + cancelable: true, + detail: event.type === "click" ? 1 : 0, + button: typeof event.which === "number" ? event.which - 1 : typeof event.button === "number" ? event.button : doc.createEvent ? 0 : 1 + }, args = _extend(defaults, event); + if (!target) { + return; + } + if (doc.createEvent && target.dispatchEvent) { + args = [ args.type, args.canBubble, args.cancelable, args.view, args.detail, args.screenX, args.screenY, args.clientX, args.clientY, args.ctrlKey, args.altKey, args.shiftKey, args.metaKey, args.button, args.relatedTarget ]; + e = doc.createEvent("MouseEvents"); + if (e.initMouseEvent) { + e.initMouseEvent.apply(e, args); + e._source = "js"; + target.dispatchEvent(e); + } + } + }; + /** + * Continuously poll the DOM until either: + * (a) the fallback content becomes visible, or + * (b) we receive an event from SWF (handled elsewhere) + * + * IMPORTANT: + * This is NOT a necessary check but it can result in significantly faster + * detection of bad `swfPath` configuration and/or network/server issues [in + * supported browsers] than waiting for the entire `flashLoadTimeout` duration + * to elapse before detecting that the SWF cannot be loaded. The detection + * duration can be anywhere from 10-30 times faster [in supported browsers] by + * using this approach. + * + * @returns `undefined` + * @private + */ + var _watchForSwfFallbackContent = function() { + var maxWait = _globalConfig.flashLoadTimeout; + if (typeof maxWait === "number" && maxWait >= 0) { + var pollWait = Math.min(1e3, maxWait / 10); + var fallbackContentId = _globalConfig.swfObjectId + "_fallbackContent"; + _swfFallbackCheckInterval = _setInterval(function() { + var el = _document.getElementById(fallbackContentId); + if (_isElementVisible(el)) { + _clearTimeoutsAndPolling(); + _flashState.deactivated = null; + ZeroClipboard.emit({ + type: "error", + name: "swf-not-found" + }); + } + }, pollWait); + } + }; + /** + * Create the HTML bridge element to embed the Flash object into. + * @private + */ + var _createHtmlBridge = function() { + var container = _document.createElement("div"); + container.id = _globalConfig.containerId; + container.className = _globalConfig.containerClass; + container.style.position = "absolute"; + container.style.left = "0px"; + container.style.top = "-9999px"; + container.style.width = "1px"; + container.style.height = "1px"; + container.style.zIndex = "" + _getSafeZIndex(_globalConfig.zIndex); + return container; + }; + /** + * Get the HTML element container that wraps the Flash bridge object/element. + * @private + */ + var _getHtmlBridge = function(flashBridge) { + var htmlBridge = flashBridge && flashBridge.parentNode; + while (htmlBridge && htmlBridge.nodeName === "OBJECT" && htmlBridge.parentNode) { + htmlBridge = htmlBridge.parentNode; + } + return htmlBridge || null; + }; + /** + * Create the SWF object. + * + * @returns The SWF object reference. + * @private + */ + var _embedSwf = function() { + var len, flashBridge = _flashState.bridge, container = _getHtmlBridge(flashBridge); + if (!flashBridge) { + var allowScriptAccess = _determineScriptAccess(_window.location.host, _globalConfig); + var allowNetworking = allowScriptAccess === "never" ? "none" : "all"; + var flashvars = _vars(_extend({ + jsVersion: ZeroClipboard.version + }, _globalConfig)); + var swfUrl = _globalConfig.swfPath + _cacheBust(_globalConfig.swfPath, _globalConfig); + container = _createHtmlBridge(); + var divToBeReplaced = _document.createElement("div"); + container.appendChild(divToBeReplaced); + _document.body.appendChild(container); + var tmpDiv = _document.createElement("div"); + var usingActiveX = _flashState.pluginType === "activex"; + tmpDiv.innerHTML = '" + (usingActiveX ? '' : "") + '' + '' + '' + '' + '' + '
 
' + "
"; + flashBridge = tmpDiv.firstChild; + tmpDiv = null; + _unwrap(flashBridge).ZeroClipboard = ZeroClipboard; + container.replaceChild(flashBridge, divToBeReplaced); + _watchForSwfFallbackContent(); + } + if (!flashBridge) { + flashBridge = _document[_globalConfig.swfObjectId]; + if (flashBridge && (len = flashBridge.length)) { + flashBridge = flashBridge[len - 1]; + } + if (!flashBridge && container) { + flashBridge = container.firstChild; + } + } + _flashState.bridge = flashBridge || null; + return flashBridge; + }; + /** + * Destroy the SWF object. + * @private + */ + var _unembedSwf = function() { + var flashBridge = _flashState.bridge; + if (flashBridge) { + var htmlBridge = _getHtmlBridge(flashBridge); + if (htmlBridge) { + if (_flashState.pluginType === "activex" && "readyState" in flashBridge) { + flashBridge.style.display = "none"; + (function removeSwfFromIE() { + if (flashBridge.readyState === 4) { + for (var prop in flashBridge) { + if (typeof flashBridge[prop] === "function") { + flashBridge[prop] = null; + } + } + if (flashBridge.parentNode) { + flashBridge.parentNode.removeChild(flashBridge); + } + if (htmlBridge.parentNode) { + htmlBridge.parentNode.removeChild(htmlBridge); + } + } else { + _setTimeout(removeSwfFromIE, 10); + } + })(); + } else { + if (flashBridge.parentNode) { + flashBridge.parentNode.removeChild(flashBridge); + } + if (htmlBridge.parentNode) { + htmlBridge.parentNode.removeChild(htmlBridge); + } + } + } + _clearTimeoutsAndPolling(); + _flashState.ready = null; + _flashState.bridge = null; + _flashState.deactivated = null; + _zcSwfVersion = undefined; + } + }; + /** + * Map the data format names of the "clipData" to Flash-friendly names. + * + * @returns A new transformed object. + * @private + */ + var _mapClipDataToFlash = function(clipData) { + var newClipData = {}, formatMap = {}; + if (!(typeof clipData === "object" && clipData)) { + return; + } + for (var dataFormat in clipData) { + if (dataFormat && _hasOwn.call(clipData, dataFormat) && typeof clipData[dataFormat] === "string" && clipData[dataFormat]) { + switch (dataFormat.toLowerCase()) { + case "text/plain": + case "text": + case "air:text": + case "flash:text": + newClipData.text = clipData[dataFormat]; + formatMap.text = dataFormat; + break; + + case "text/html": + case "html": + case "air:html": + case "flash:html": + newClipData.html = clipData[dataFormat]; + formatMap.html = dataFormat; + break; + + case "application/rtf": + case "text/rtf": + case "rtf": + case "richtext": + case "air:rtf": + case "flash:rtf": + newClipData.rtf = clipData[dataFormat]; + formatMap.rtf = dataFormat; + break; + + default: + break; + } + } + } + return { + data: newClipData, + formatMap: formatMap + }; + }; + /** + * Map the data format names from Flash-friendly names back to their original "clipData" names (via a format mapping). + * + * @returns A new transformed object. + * @private + */ + var _mapClipResultsFromFlash = function(clipResults, formatMap) { + if (!(typeof clipResults === "object" && clipResults && typeof formatMap === "object" && formatMap)) { + return clipResults; + } + var newResults = {}; + for (var prop in clipResults) { + if (_hasOwn.call(clipResults, prop)) { + if (prop === "errors") { + newResults[prop] = clipResults[prop] ? clipResults[prop].slice() : []; + for (var i = 0, len = newResults[prop].length; i < len; i++) { + newResults[prop][i].format = formatMap[newResults[prop][i].format]; + } + } else if (prop !== "success" && prop !== "data") { + newResults[prop] = clipResults[prop]; + } else { + newResults[prop] = {}; + var tmpHash = clipResults[prop]; + for (var dataFormat in tmpHash) { + if (dataFormat && _hasOwn.call(tmpHash, dataFormat) && _hasOwn.call(formatMap, dataFormat)) { + newResults[prop][formatMap[dataFormat]] = tmpHash[dataFormat]; + } + } + } + } + } + return newResults; + }; + /** + * Will look at a path, and will create a "?noCache={time}" or "&noCache={time}" + * query param string to return. Does NOT append that string to the original path. + * This is useful because ExternalInterface often breaks when a Flash SWF is cached. + * + * @returns The `noCache` query param with necessary "?"/"&" prefix. + * @private + */ + var _cacheBust = function(path, options) { + var cacheBust = options == null || options && options.cacheBust === true; + if (cacheBust) { + return (path.indexOf("?") === -1 ? "?" : "&") + "noCache=" + _now(); + } else { + return ""; + } + }; + /** + * Creates a query string for the FlashVars param. + * Does NOT include the cache-busting query param. + * + * @returns FlashVars query string + * @private + */ + var _vars = function(options) { + var i, len, domain, domains, str = "", trustedOriginsExpanded = []; + if (options.trustedDomains) { + if (typeof options.trustedDomains === "string") { + domains = [ options.trustedDomains ]; + } else if (typeof options.trustedDomains === "object" && "length" in options.trustedDomains) { + domains = options.trustedDomains; + } + } + if (domains && domains.length) { + for (i = 0, len = domains.length; i < len; i++) { + if (_hasOwn.call(domains, i) && domains[i] && typeof domains[i] === "string") { + domain = _extractDomain(domains[i]); + if (!domain) { + continue; + } + if (domain === "*") { + trustedOriginsExpanded.length = 0; + trustedOriginsExpanded.push(domain); + break; + } + trustedOriginsExpanded.push.apply(trustedOriginsExpanded, [ domain, "//" + domain, _window.location.protocol + "//" + domain ]); + } + } + } + if (trustedOriginsExpanded.length) { + str += "trustedOrigins=" + _encodeURIComponent(trustedOriginsExpanded.join(",")); + } + if (options.forceEnhancedClipboard === true) { + str += (str ? "&" : "") + "forceEnhancedClipboard=true"; + } + if (typeof options.swfObjectId === "string" && options.swfObjectId) { + str += (str ? "&" : "") + "swfObjectId=" + _encodeURIComponent(options.swfObjectId); + } + if (typeof options.jsVersion === "string" && options.jsVersion) { + str += (str ? "&" : "") + "jsVersion=" + _encodeURIComponent(options.jsVersion); + } + return str; + }; + /** + * Extract the domain (e.g. "github.com") from an origin (e.g. "https://github.com") or + * URL (e.g. "https://github.com/zeroclipboard/zeroclipboard/"). + * + * @returns the domain + * @private + */ + var _extractDomain = function(originOrUrl) { + if (originOrUrl == null || originOrUrl === "") { + return null; + } + originOrUrl = originOrUrl.replace(/^\s+|\s+$/g, ""); + if (originOrUrl === "") { + return null; + } + var protocolIndex = originOrUrl.indexOf("//"); + originOrUrl = protocolIndex === -1 ? originOrUrl : originOrUrl.slice(protocolIndex + 2); + var pathIndex = originOrUrl.indexOf("/"); + originOrUrl = pathIndex === -1 ? originOrUrl : protocolIndex === -1 || pathIndex === 0 ? null : originOrUrl.slice(0, pathIndex); + if (originOrUrl && originOrUrl.slice(-4).toLowerCase() === ".swf") { + return null; + } + return originOrUrl || null; + }; + /** + * Set `allowScriptAccess` based on `trustedDomains` and `window.location.host` vs. `swfPath`. + * + * @returns The appropriate script access level. + * @private + */ + var _determineScriptAccess = function() { + var _extractAllDomains = function(origins) { + var i, len, tmp, resultsArray = []; + if (typeof origins === "string") { + origins = [ origins ]; + } + if (!(typeof origins === "object" && origins && typeof origins.length === "number")) { + return resultsArray; + } + for (i = 0, len = origins.length; i < len; i++) { + if (_hasOwn.call(origins, i) && (tmp = _extractDomain(origins[i]))) { + if (tmp === "*") { + resultsArray.length = 0; + resultsArray.push("*"); + break; + } + if (resultsArray.indexOf(tmp) === -1) { + resultsArray.push(tmp); + } + } + } + return resultsArray; + }; + return function(currentDomain, configOptions) { + var swfDomain = _extractDomain(configOptions.swfPath); + if (swfDomain === null) { + swfDomain = currentDomain; + } + var trustedDomains = _extractAllDomains(configOptions.trustedDomains); + var len = trustedDomains.length; + if (len > 0) { + if (len === 1 && trustedDomains[0] === "*") { + return "always"; + } + if (trustedDomains.indexOf(currentDomain) !== -1) { + if (len === 1 && currentDomain === swfDomain) { + return "sameDomain"; + } + return "always"; + } + } + return "never"; + }; + }(); + /** + * Get the currently active/focused DOM element. + * + * @returns the currently active/focused element, or `null` + * @private + */ + var _safeActiveElement = function() { + try { + return _document.activeElement; + } catch (err) { + return null; + } + }; + /** + * Add a class to an element, if it doesn't already have it. + * + * @returns The element, with its new class added. + * @private + */ + var _addClass = function(element, value) { + var c, cl, className, classNames = []; + if (typeof value === "string" && value) { + classNames = value.split(/\s+/); + } + if (element && element.nodeType === 1 && classNames.length > 0) { + className = (" " + (element.className || "") + " ").replace(/[\t\r\n\f]/g, " "); + for (c = 0, cl = classNames.length; c < cl; c++) { + if (className.indexOf(" " + classNames[c] + " ") === -1) { + className += classNames[c] + " "; + } + } + className = className.replace(/^\s+|\s+$/g, ""); + if (className !== element.className) { + element.className = className; + } + } + return element; + }; + /** + * Remove a class from an element, if it has it. + * + * @returns The element, with its class removed. + * @private + */ + var _removeClass = function(element, value) { + var c, cl, className, classNames = []; + if (typeof value === "string" && value) { + classNames = value.split(/\s+/); + } + if (element && element.nodeType === 1 && classNames.length > 0) { + if (element.className) { + className = (" " + element.className + " ").replace(/[\t\r\n\f]/g, " "); + for (c = 0, cl = classNames.length; c < cl; c++) { + className = className.replace(" " + classNames[c] + " ", " "); + } + className = className.replace(/^\s+|\s+$/g, ""); + if (className !== element.className) { + element.className = className; + } + } + } + return element; + }; + /** + * Attempt to interpret the element's CSS styling. If `prop` is `"cursor"`, + * then we assume that it should be a hand ("pointer") cursor if the element + * is an anchor element ("a" tag). + * + * @returns The computed style property. + * @private + */ + var _getStyle = function(el, prop) { + var value = _getComputedStyle(el, null).getPropertyValue(prop); + if (prop === "cursor") { + if (!value || value === "auto") { + if (el.nodeName === "A") { + return "pointer"; + } + } + } + return value; + }; + /** + * Get the absolutely positioned coordinates of a DOM element. + * + * @returns Object containing the element's position, width, and height. + * @private + */ + var _getElementPosition = function(el) { + var pos = { + left: 0, + top: 0, + width: 0, + height: 0 + }; + if (el.getBoundingClientRect) { + var elRect = el.getBoundingClientRect(); + var pageXOffset = _window.pageXOffset; + var pageYOffset = _window.pageYOffset; + var leftBorderWidth = _document.documentElement.clientLeft || 0; + var topBorderWidth = _document.documentElement.clientTop || 0; + var leftBodyOffset = 0; + var topBodyOffset = 0; + if (_getStyle(_document.body, "position") === "relative") { + var bodyRect = _document.body.getBoundingClientRect(); + var htmlRect = _document.documentElement.getBoundingClientRect(); + leftBodyOffset = bodyRect.left - htmlRect.left || 0; + topBodyOffset = bodyRect.top - htmlRect.top || 0; + } + pos.left = elRect.left + pageXOffset - leftBorderWidth - leftBodyOffset; + pos.top = elRect.top + pageYOffset - topBorderWidth - topBodyOffset; + pos.width = "width" in elRect ? elRect.width : elRect.right - elRect.left; + pos.height = "height" in elRect ? elRect.height : elRect.bottom - elRect.top; + } + return pos; + }; + /** + * Determine is an element is visible somewhere within the document (page). + * + * @returns Boolean + * @private + */ + var _isElementVisible = function(el) { + if (!el) { + return false; + } + var styles = _getComputedStyle(el, null); + if (!styles) { + return false; + } + var hasCssHeight = _parseFloat(styles.height) > 0; + var hasCssWidth = _parseFloat(styles.width) > 0; + var hasCssTop = _parseFloat(styles.top) >= 0; + var hasCssLeft = _parseFloat(styles.left) >= 0; + var cssKnows = hasCssHeight && hasCssWidth && hasCssTop && hasCssLeft; + var rect = cssKnows ? null : _getElementPosition(el); + var isVisible = styles.display !== "none" && styles.visibility !== "collapse" && (cssKnows || !!rect && (hasCssHeight || rect.height > 0) && (hasCssWidth || rect.width > 0) && (hasCssTop || rect.top >= 0) && (hasCssLeft || rect.left >= 0)); + return isVisible; + }; + /** + * Clear all existing timeouts and interval polling delegates. + * + * @returns `undefined` + * @private + */ + var _clearTimeoutsAndPolling = function() { + _clearTimeout(_flashCheckTimeout); + _flashCheckTimeout = 0; + _clearInterval(_swfFallbackCheckInterval); + _swfFallbackCheckInterval = 0; + }; + /** + * Reposition the Flash object to cover the currently activated element. + * + * @returns `undefined` + * @private + */ + var _reposition = function() { + var htmlBridge; + if (_currentElement && (htmlBridge = _getHtmlBridge(_flashState.bridge))) { + var pos = _getElementPosition(_currentElement); + _extend(htmlBridge.style, { + width: pos.width + "px", + height: pos.height + "px", + top: pos.top + "px", + left: pos.left + "px", + zIndex: "" + _getSafeZIndex(_globalConfig.zIndex) + }); + } + }; + /** + * Sends a signal to the Flash object to display the hand cursor if `true`. + * + * @returns `undefined` + * @private + */ + var _setHandCursor = function(enabled) { + if (_flashState.ready === true) { + if (_flashState.bridge && typeof _flashState.bridge.setHandCursor === "function") { + _flashState.bridge.setHandCursor(enabled); + } else { + _flashState.ready = false; + } + } + }; + /** + * Get a safe value for `zIndex` + * + * @returns an integer, or "auto" + * @private + */ + var _getSafeZIndex = function(val) { + if (/^(?:auto|inherit)$/.test(val)) { + return val; + } + var zIndex; + if (typeof val === "number" && !_isNaN(val)) { + zIndex = val; + } else if (typeof val === "string") { + zIndex = _getSafeZIndex(_parseInt(val, 10)); + } + return typeof zIndex === "number" ? zIndex : "auto"; + }; + /** + * Ensure OS-compliant line endings, i.e. "\r\n" on Windows, "\n" elsewhere + * + * @returns string + * @private + */ + var _fixLineEndings = function(content) { + var replaceRegex = /(\r\n|\r|\n)/g; + if (typeof content === "string" && _globalConfig.fixLineEndings === true) { + if (_isWindows()) { + if (/((^|[^\r])\n|\r([^\n]|$))/.test(content)) { + content = content.replace(replaceRegex, "\r\n"); + } + } else if (/\r/.test(content)) { + content = content.replace(replaceRegex, "\n"); + } + } + return content; + }; + /** + * Attempt to detect if ZeroClipboard is executing inside of a sandboxed iframe. + * If it is, Flash Player cannot be used, so ZeroClipboard is dead in the water. + * + * @see {@link http://lists.w3.org/Archives/Public/public-whatwg-archive/2014Dec/0002.html} + * @see {@link https://github.com/zeroclipboard/zeroclipboard/issues/511} + * @see {@link http://zeroclipboard.org/test-iframes.html} + * + * @returns `true` (is sandboxed), `false` (is not sandboxed), or `null` (uncertain) + * @private + */ + var _detectSandbox = function(doNotReassessFlashSupport) { + var effectiveScriptOrigin, frame, frameError, previousState = _flashState.sandboxed, isSandboxed = null; + doNotReassessFlashSupport = doNotReassessFlashSupport === true; + if (_pageIsFramed === false) { + isSandboxed = false; + } else { + try { + frame = window.frameElement || null; + } catch (e) { + frameError = { + name: e.name, + message: e.message + }; + } + if (frame && frame.nodeType === 1 && frame.nodeName === "IFRAME") { + try { + isSandboxed = frame.hasAttribute("sandbox"); + } catch (e) { + isSandboxed = null; + } + } else { + try { + effectiveScriptOrigin = document.domain || null; + } catch (e) { + effectiveScriptOrigin = null; + } + if (effectiveScriptOrigin === null || frameError && frameError.name === "SecurityError" && /(^|[\s\(\[@])sandbox(es|ed|ing|[\s\.,!\)\]@]|$)/.test(frameError.message.toLowerCase())) { + isSandboxed = true; + } + } + } + _flashState.sandboxed = isSandboxed; + if (previousState !== isSandboxed && !doNotReassessFlashSupport) { + _detectFlashSupport(_ActiveXObject); + } + return isSandboxed; + }; + /** + * Detect the Flash Player status, version, and plugin type. + * + * @see {@link https://code.google.com/p/doctype-mirror/wiki/ArticleDetectFlash#The_code} + * @see {@link http://stackoverflow.com/questions/12866060/detecting-pepper-ppapi-flash-with-javascript} + * + * @returns `undefined` + * @private + */ + var _detectFlashSupport = function(ActiveXObject) { + var plugin, ax, mimeType, hasFlash = false, isActiveX = false, isPPAPI = false, flashVersion = ""; + /** + * Derived from Apple's suggested sniffer. + * @param {String} desc e.g. "Shockwave Flash 7.0 r61" + * @returns {String} "7.0.61" + * @private + */ + function parseFlashVersion(desc) { + var matches = desc.match(/[\d]+/g); + matches.length = 3; + return matches.join("."); + } + function isPepperFlash(flashPlayerFileName) { + return !!flashPlayerFileName && (flashPlayerFileName = flashPlayerFileName.toLowerCase()) && (/^(pepflashplayer\.dll|libpepflashplayer\.so|pepperflashplayer\.plugin)$/.test(flashPlayerFileName) || flashPlayerFileName.slice(-13) === "chrome.plugin"); + } + function inspectPlugin(plugin) { + if (plugin) { + hasFlash = true; + if (plugin.version) { + flashVersion = parseFlashVersion(plugin.version); + } + if (!flashVersion && plugin.description) { + flashVersion = parseFlashVersion(plugin.description); + } + if (plugin.filename) { + isPPAPI = isPepperFlash(plugin.filename); + } + } + } + if (_navigator.plugins && _navigator.plugins.length) { + plugin = _navigator.plugins["Shockwave Flash"]; + inspectPlugin(plugin); + if (_navigator.plugins["Shockwave Flash 2.0"]) { + hasFlash = true; + flashVersion = "2.0.0.11"; + } + } else if (_navigator.mimeTypes && _navigator.mimeTypes.length) { + mimeType = _navigator.mimeTypes["application/x-shockwave-flash"]; + plugin = mimeType && mimeType.enabledPlugin; + inspectPlugin(plugin); + } else if (typeof ActiveXObject !== "undefined") { + isActiveX = true; + try { + ax = new ActiveXObject("ShockwaveFlash.ShockwaveFlash.7"); + hasFlash = true; + flashVersion = parseFlashVersion(ax.GetVariable("$version")); + } catch (e1) { + try { + ax = new ActiveXObject("ShockwaveFlash.ShockwaveFlash.6"); + hasFlash = true; + flashVersion = "6.0.21"; + } catch (e2) { + try { + ax = new ActiveXObject("ShockwaveFlash.ShockwaveFlash"); + hasFlash = true; + flashVersion = parseFlashVersion(ax.GetVariable("$version")); + } catch (e3) { + isActiveX = false; + } + } + } + } + _flashState.disabled = hasFlash !== true; + _flashState.outdated = flashVersion && _parseFloat(flashVersion) < _parseFloat(_minimumFlashVersion); + _flashState.version = flashVersion || "0.0.0"; + _flashState.pluginType = isPPAPI ? "pepper" : isActiveX ? "activex" : hasFlash ? "netscape" : "unknown"; + }; + /** + * Invoke the Flash detection algorithms immediately upon inclusion so we're not waiting later. + */ + _detectFlashSupport(_ActiveXObject); + /** + * Always assess the `sandboxed` state of the page at important Flash-related moments. + */ + _detectSandbox(true); + /** + * A shell constructor for `ZeroClipboard` client instances. + * + * @constructor + */ + var ZeroClipboard = function() { + if (!(this instanceof ZeroClipboard)) { + return new ZeroClipboard(); + } + if (typeof ZeroClipboard._createClient === "function") { + ZeroClipboard._createClient.apply(this, _args(arguments)); + } + }; + /** + * The ZeroClipboard library's version number. + * + * @static + * @readonly + * @property {string} + */ + _defineProperty(ZeroClipboard, "version", { + value: "2.3.0-beta.1", + writable: false, + configurable: true, + enumerable: true + }); + /** + * Update or get a copy of the ZeroClipboard global configuration. + * Returns a copy of the current/updated configuration. + * + * @returns Object + * @static + */ + ZeroClipboard.config = function() { + return _config.apply(this, _args(arguments)); + }; + /** + * Diagnostic method that describes the state of the browser, Flash Player, and ZeroClipboard. + * + * @returns Object + * @static + */ + ZeroClipboard.state = function() { + return _state.apply(this, _args(arguments)); + }; + /** + * Check if Flash is unusable for any reason: disabled, outdated, deactivated, etc. + * + * @returns Boolean + * @static + */ + ZeroClipboard.isFlashUnusable = function() { + return _isFlashUnusable.apply(this, _args(arguments)); + }; + /** + * Register an event listener. + * + * @returns `ZeroClipboard` + * @static + */ + ZeroClipboard.on = function() { + return _on.apply(this, _args(arguments)); + }; + /** + * Unregister an event listener. + * If no `listener` function/object is provided, it will unregister all listeners for the provided `eventType`. + * If no `eventType` is provided, it will unregister all listeners for every event type. + * + * @returns `ZeroClipboard` + * @static + */ + ZeroClipboard.off = function() { + return _off.apply(this, _args(arguments)); + }; + /** + * Retrieve event listeners for an `eventType`. + * If no `eventType` is provided, it will retrieve all listeners for every event type. + * + * @returns array of listeners for the `eventType`; if no `eventType`, then a map/hash object of listeners for all event types; or `null` + */ + ZeroClipboard.handlers = function() { + return _listeners.apply(this, _args(arguments)); + }; + /** + * Event emission receiver from the Flash object, forwarding to any registered JavaScript event listeners. + * + * @returns For the "copy" event, returns the Flash-friendly "clipData" object; otherwise `undefined`. + * @static + */ + ZeroClipboard.emit = function() { + return _emit.apply(this, _args(arguments)); + }; + /** + * Create and embed the Flash object. + * + * @returns The Flash object + * @static + */ + ZeroClipboard.create = function() { + return _create.apply(this, _args(arguments)); + }; + /** + * Self-destruct and clean up everything, including the embedded Flash object. + * + * @returns `undefined` + * @static + */ + ZeroClipboard.destroy = function() { + return _destroy.apply(this, _args(arguments)); + }; + /** + * Set the pending data for clipboard injection. + * + * @returns `undefined` + * @static + */ + ZeroClipboard.setData = function() { + return _setData.apply(this, _args(arguments)); + }; + /** + * Clear the pending data for clipboard injection. + * If no `format` is provided, all pending data formats will be cleared. + * + * @returns `undefined` + * @static + */ + ZeroClipboard.clearData = function() { + return _clearData.apply(this, _args(arguments)); + }; + /** + * Get a copy of the pending data for clipboard injection. + * If no `format` is provided, a copy of ALL pending data formats will be returned. + * + * @returns `String` or `Object` + * @static + */ + ZeroClipboard.getData = function() { + return _getData.apply(this, _args(arguments)); + }; + /** + * Sets the current HTML object that the Flash object should overlay. This will put the global + * Flash object on top of the current element; depending on the setup, this may also set the + * pending clipboard text data as well as the Flash object's wrapping element's title attribute + * based on the underlying HTML element and ZeroClipboard configuration. + * + * @returns `undefined` + * @static + */ + ZeroClipboard.focus = ZeroClipboard.activate = function() { + return _focus.apply(this, _args(arguments)); + }; + /** + * Un-overlays the Flash object. This will put the global Flash object off-screen; depending on + * the setup, this may also unset the Flash object's wrapping element's title attribute based on + * the underlying HTML element and ZeroClipboard configuration. + * + * @returns `undefined` + * @static + */ + ZeroClipboard.blur = ZeroClipboard.deactivate = function() { + return _blur.apply(this, _args(arguments)); + }; + /** + * Returns the currently focused/"activated" HTML element that the Flash object is wrapping. + * + * @returns `HTMLElement` or `null` + * @static + */ + ZeroClipboard.activeElement = function() { + return _activeElement.apply(this, _args(arguments)); + }; + /** + * Keep track of the ZeroClipboard client instance counter. + */ + var _clientIdCounter = 0; + /** + * Keep track of the state of the client instances. + * + * Entry structure: + * _clientMeta[client.id] = { + * instance: client, + * elements: [], + * handlers: {} + * }; + */ + var _clientMeta = {}; + /** + * Keep track of the ZeroClipboard clipped elements counter. + */ + var _elementIdCounter = 0; + /** + * Keep track of the state of the clipped element relationships to clients. + * + * Entry structure: + * _elementMeta[element.zcClippingId] = [client1.id, client2.id]; + */ + var _elementMeta = {}; + /** + * Keep track of the state of the mouse event handlers for clipped elements. + * + * Entry structure: + * _mouseHandlers[element.zcClippingId] = { + * mouseover: function(event) {}, + * mouseout: function(event) {}, + * mouseenter: function(event) {}, + * mouseleave: function(event) {}, + * mousemove: function(event) {} + * }; + */ + var _mouseHandlers = {}; + /** + * Extending the ZeroClipboard configuration defaults for the Client module. + */ + _extend(_globalConfig, { + autoActivate: true + }); + /** + * The real constructor for `ZeroClipboard` client instances. + * @private + */ + var _clientConstructor = function(elements) { + var client = this; + client.id = "" + _clientIdCounter++; + _clientMeta[client.id] = { + instance: client, + elements: [], + handlers: {} + }; + if (elements) { + client.clip(elements); + } + ZeroClipboard.on("*", function(event) { + return client.emit(event); + }); + ZeroClipboard.on("destroy", function() { + client.destroy(); + }); + ZeroClipboard.create(); + }; + /** + * The underlying implementation of `ZeroClipboard.Client.prototype.on`. + * @private + */ + var _clientOn = function(eventType, listener) { + var i, len, events, added = {}, meta = _clientMeta[this.id], handlers = meta && meta.handlers; + if (!meta) { + throw new Error("Attempted to add new listener(s) to a destroyed ZeroClipboard client instance"); + } + if (typeof eventType === "string" && eventType) { + events = eventType.toLowerCase().split(/\s+/); + } else if (typeof eventType === "object" && eventType && typeof listener === "undefined") { + for (i in eventType) { + if (_hasOwn.call(eventType, i) && typeof i === "string" && i && typeof eventType[i] === "function") { + this.on(i, eventType[i]); + } + } + } + if (events && events.length) { + for (i = 0, len = events.length; i < len; i++) { + eventType = events[i].replace(/^on/, ""); + added[eventType] = true; + if (!handlers[eventType]) { + handlers[eventType] = []; + } + handlers[eventType].push(listener); + } + if (added.ready && _flashState.ready) { + this.emit({ + type: "ready", + client: this + }); + } + if (added.error) { + for (i = 0, len = _flashStateErrorNames.length; i < len; i++) { + if (_flashState[_flashStateErrorNames[i].replace(/^flash-/, "")]) { + this.emit({ + type: "error", + name: _flashStateErrorNames[i], + client: this + }); + break; + } + } + if (_zcSwfVersion !== undefined && ZeroClipboard.version !== _zcSwfVersion) { + this.emit({ + type: "error", + name: "version-mismatch", + jsVersion: ZeroClipboard.version, + swfVersion: _zcSwfVersion + }); + } + } + } + return this; + }; + /** + * The underlying implementation of `ZeroClipboard.Client.prototype.off`. + * @private + */ + var _clientOff = function(eventType, listener) { + var i, len, foundIndex, events, perEventHandlers, meta = _clientMeta[this.id], handlers = meta && meta.handlers; + if (!handlers) { + return this; + } + if (arguments.length === 0) { + events = _keys(handlers); + } else if (typeof eventType === "string" && eventType) { + events = eventType.split(/\s+/); + } else if (typeof eventType === "object" && eventType && typeof listener === "undefined") { + for (i in eventType) { + if (_hasOwn.call(eventType, i) && typeof i === "string" && i && typeof eventType[i] === "function") { + this.off(i, eventType[i]); + } + } + } + if (events && events.length) { + for (i = 0, len = events.length; i < len; i++) { + eventType = events[i].toLowerCase().replace(/^on/, ""); + perEventHandlers = handlers[eventType]; + if (perEventHandlers && perEventHandlers.length) { + if (listener) { + foundIndex = perEventHandlers.indexOf(listener); + while (foundIndex !== -1) { + perEventHandlers.splice(foundIndex, 1); + foundIndex = perEventHandlers.indexOf(listener, foundIndex); + } + } else { + perEventHandlers.length = 0; + } + } + } + } + return this; + }; + /** + * The underlying implementation of `ZeroClipboard.Client.prototype.handlers`. + * @private + */ + var _clientListeners = function(eventType) { + var copy = null, handlers = _clientMeta[this.id] && _clientMeta[this.id].handlers; + if (handlers) { + if (typeof eventType === "string" && eventType) { + copy = handlers[eventType] ? handlers[eventType].slice(0) : []; + } else { + copy = _deepCopy(handlers); + } + } + return copy; + }; + /** + * The underlying implementation of `ZeroClipboard.Client.prototype.emit`. + * @private + */ + var _clientEmit = function(event) { + if (_clientShouldEmit.call(this, event)) { + if (typeof event === "object" && event && typeof event.type === "string" && event.type) { + event = _extend({}, event); + } + var eventCopy = _extend({}, _createEvent(event), { + client: this + }); + _clientDispatchCallbacks.call(this, eventCopy); + } + return this; + }; + /** + * The underlying implementation of `ZeroClipboard.Client.prototype.clip`. + * @private + */ + var _clientClip = function(elements) { + if (!_clientMeta[this.id]) { + throw new Error("Attempted to clip element(s) to a destroyed ZeroClipboard client instance"); + } + elements = _prepClip(elements); + for (var i = 0; i < elements.length; i++) { + if (_hasOwn.call(elements, i) && elements[i] && elements[i].nodeType === 1) { + if (!elements[i].zcClippingId) { + elements[i].zcClippingId = "zcClippingId_" + _elementIdCounter++; + _elementMeta[elements[i].zcClippingId] = [ this.id ]; + if (_globalConfig.autoActivate === true) { + _addMouseHandlers(elements[i]); + } + } else if (_elementMeta[elements[i].zcClippingId].indexOf(this.id) === -1) { + _elementMeta[elements[i].zcClippingId].push(this.id); + } + var clippedElements = _clientMeta[this.id] && _clientMeta[this.id].elements; + if (clippedElements.indexOf(elements[i]) === -1) { + clippedElements.push(elements[i]); + } + } + } + return this; + }; + /** + * The underlying implementation of `ZeroClipboard.Client.prototype.unclip`. + * @private + */ + var _clientUnclip = function(elements) { + var meta = _clientMeta[this.id]; + if (!meta) { + return this; + } + var clippedElements = meta.elements; + var arrayIndex; + if (typeof elements === "undefined") { + elements = clippedElements.slice(0); + } else { + elements = _prepClip(elements); + } + for (var i = elements.length; i--; ) { + if (_hasOwn.call(elements, i) && elements[i] && elements[i].nodeType === 1) { + arrayIndex = 0; + while ((arrayIndex = clippedElements.indexOf(elements[i], arrayIndex)) !== -1) { + clippedElements.splice(arrayIndex, 1); + } + var clientIds = _elementMeta[elements[i].zcClippingId]; + if (clientIds) { + arrayIndex = 0; + while ((arrayIndex = clientIds.indexOf(this.id, arrayIndex)) !== -1) { + clientIds.splice(arrayIndex, 1); + } + if (clientIds.length === 0) { + if (_globalConfig.autoActivate === true) { + _removeMouseHandlers(elements[i]); + } + delete elements[i].zcClippingId; + } + } + } + } + return this; + }; + /** + * The underlying implementation of `ZeroClipboard.Client.prototype.elements`. + * @private + */ + var _clientElements = function() { + var meta = _clientMeta[this.id]; + return meta && meta.elements ? meta.elements.slice(0) : []; + }; + /** + * The underlying implementation of `ZeroClipboard.Client.prototype.destroy`. + * @private + */ + var _clientDestroy = function() { + if (!_clientMeta[this.id]) { + return; + } + this.unclip(); + this.off(); + delete _clientMeta[this.id]; + }; + /** + * Inspect an Event to see if the Client (`this`) should honor it for emission. + * @private + */ + var _clientShouldEmit = function(event) { + if (!(event && event.type)) { + return false; + } + if (event.client && event.client !== this) { + return false; + } + var meta = _clientMeta[this.id]; + var clippedEls = meta && meta.elements; + var hasClippedEls = !!clippedEls && clippedEls.length > 0; + var goodTarget = !event.target || hasClippedEls && clippedEls.indexOf(event.target) !== -1; + var goodRelTarget = event.relatedTarget && hasClippedEls && clippedEls.indexOf(event.relatedTarget) !== -1; + var goodClient = event.client && event.client === this; + if (!meta || !(goodTarget || goodRelTarget || goodClient)) { + return false; + } + return true; + }; + /** + * Handle the actual dispatching of events to a client instance. + * + * @returns `undefined` + * @private + */ + var _clientDispatchCallbacks = function(event) { + var meta = _clientMeta[this.id]; + if (!(typeof event === "object" && event && event.type && meta)) { + return; + } + var async = _shouldPerformAsync(event); + var wildcardTypeHandlers = meta && meta.handlers["*"] || []; + var specificTypeHandlers = meta && meta.handlers[event.type] || []; + var handlers = wildcardTypeHandlers.concat(specificTypeHandlers); + if (handlers && handlers.length) { + var i, len, func, context, eventCopy, originalContext = this; + for (i = 0, len = handlers.length; i < len; i++) { + func = handlers[i]; + context = originalContext; + if (typeof func === "string" && typeof _window[func] === "function") { + func = _window[func]; + } + if (typeof func === "object" && func && typeof func.handleEvent === "function") { + context = func; + func = func.handleEvent; + } + if (typeof func === "function") { + eventCopy = _extend({}, event); + _dispatchCallback(func, context, [ eventCopy ], async); + } + } + } + }; + /** + * Prepares the elements for clipping/unclipping. + * + * @returns An Array of elements. + * @private + */ + var _prepClip = function(elements) { + if (typeof elements === "string") { + elements = []; + } + return typeof elements.length !== "number" ? [ elements ] : elements; + }; + /** + * Add a `mouseover` handler function for a clipped element. + * + * @returns `undefined` + * @private + */ + var _addMouseHandlers = function(element) { + if (!(element && element.nodeType === 1)) { + return; + } + var _suppressMouseEvents = function(event) { + if (!(event || (event = _window.event))) { + return; + } + if (event._source !== "js") { + event.stopImmediatePropagation(); + event.preventDefault(); + } + delete event._source; + }; + var _elementMouseOver = function(event) { + if (!(event || (event = _window.event))) { + return; + } + _suppressMouseEvents(event); + ZeroClipboard.focus(element); + }; + element.addEventListener("mouseover", _elementMouseOver, false); + element.addEventListener("mouseout", _suppressMouseEvents, false); + element.addEventListener("mouseenter", _suppressMouseEvents, false); + element.addEventListener("mouseleave", _suppressMouseEvents, false); + element.addEventListener("mousemove", _suppressMouseEvents, false); + _mouseHandlers[element.zcClippingId] = { + mouseover: _elementMouseOver, + mouseout: _suppressMouseEvents, + mouseenter: _suppressMouseEvents, + mouseleave: _suppressMouseEvents, + mousemove: _suppressMouseEvents + }; + }; + /** + * Remove a `mouseover` handler function for a clipped element. + * + * @returns `undefined` + * @private + */ + var _removeMouseHandlers = function(element) { + if (!(element && element.nodeType === 1)) { + return; + } + var mouseHandlers = _mouseHandlers[element.zcClippingId]; + if (!(typeof mouseHandlers === "object" && mouseHandlers)) { + return; + } + var key, val, mouseEvents = [ "move", "leave", "enter", "out", "over" ]; + for (var i = 0, len = mouseEvents.length; i < len; i++) { + key = "mouse" + mouseEvents[i]; + val = mouseHandlers[key]; + if (typeof val === "function") { + element.removeEventListener(key, val, false); + } + } + delete _mouseHandlers[element.zcClippingId]; + }; + /** + * Creates a new ZeroClipboard client instance. + * Optionally, auto-`clip` an element or collection of elements. + * + * @constructor + */ + ZeroClipboard._createClient = function() { + _clientConstructor.apply(this, _args(arguments)); + }; + /** + * Register an event listener to the client. + * + * @returns `this` + */ + ZeroClipboard.prototype.on = function() { + return _clientOn.apply(this, _args(arguments)); + }; + /** + * Unregister an event handler from the client. + * If no `listener` function/object is provided, it will unregister all handlers for the provided `eventType`. + * If no `eventType` is provided, it will unregister all handlers for every event type. + * + * @returns `this` + */ + ZeroClipboard.prototype.off = function() { + return _clientOff.apply(this, _args(arguments)); + }; + /** + * Retrieve event listeners for an `eventType` from the client. + * If no `eventType` is provided, it will retrieve all listeners for every event type. + * + * @returns array of listeners for the `eventType`; if no `eventType`, then a map/hash object of listeners for all event types; or `null` + */ + ZeroClipboard.prototype.handlers = function() { + return _clientListeners.apply(this, _args(arguments)); + }; + /** + * Event emission receiver from the Flash object for this client's registered JavaScript event listeners. + * + * @returns For the "copy" event, returns the Flash-friendly "clipData" object; otherwise `undefined`. + */ + ZeroClipboard.prototype.emit = function() { + return _clientEmit.apply(this, _args(arguments)); + }; + /** + * Register clipboard actions for new element(s) to the client. + * + * @returns `this` + */ + ZeroClipboard.prototype.clip = function() { + return _clientClip.apply(this, _args(arguments)); + }; + /** + * Unregister the clipboard actions of previously registered element(s) on the page. + * If no elements are provided, ALL registered elements will be unregistered. + * + * @returns `this` + */ + ZeroClipboard.prototype.unclip = function() { + return _clientUnclip.apply(this, _args(arguments)); + }; + /** + * Get all of the elements to which this client is clipped. + * + * @returns array of clipped elements + */ + ZeroClipboard.prototype.elements = function() { + return _clientElements.apply(this, _args(arguments)); + }; + /** + * Self-destruct and clean up everything for a single client. + * This will NOT destroy the embedded Flash object. + * + * @returns `undefined` + */ + ZeroClipboard.prototype.destroy = function() { + return _clientDestroy.apply(this, _args(arguments)); + }; + /** + * Stores the pending plain text to inject into the clipboard. + * + * @returns `this` + */ + ZeroClipboard.prototype.setText = function(text) { + if (!_clientMeta[this.id]) { + throw new Error("Attempted to set pending clipboard data from a destroyed ZeroClipboard client instance"); + } + ZeroClipboard.setData("text/plain", text); + return this; + }; + /** + * Stores the pending HTML text to inject into the clipboard. + * + * @returns `this` + */ + ZeroClipboard.prototype.setHtml = function(html) { + if (!_clientMeta[this.id]) { + throw new Error("Attempted to set pending clipboard data from a destroyed ZeroClipboard client instance"); + } + ZeroClipboard.setData("text/html", html); + return this; + }; + /** + * Stores the pending rich text (RTF) to inject into the clipboard. + * + * @returns `this` + */ + ZeroClipboard.prototype.setRichText = function(richText) { + if (!_clientMeta[this.id]) { + throw new Error("Attempted to set pending clipboard data from a destroyed ZeroClipboard client instance"); + } + ZeroClipboard.setData("application/rtf", richText); + return this; + }; + /** + * Stores the pending data to inject into the clipboard. + * + * @returns `this` + */ + ZeroClipboard.prototype.setData = function() { + if (!_clientMeta[this.id]) { + throw new Error("Attempted to set pending clipboard data from a destroyed ZeroClipboard client instance"); + } + ZeroClipboard.setData.apply(this, _args(arguments)); + return this; + }; + /** + * Clears the pending data to inject into the clipboard. + * If no `format` is provided, all pending data formats will be cleared. + * + * @returns `this` + */ + ZeroClipboard.prototype.clearData = function() { + if (!_clientMeta[this.id]) { + throw new Error("Attempted to clear pending clipboard data from a destroyed ZeroClipboard client instance"); + } + ZeroClipboard.clearData.apply(this, _args(arguments)); + return this; + }; + /** + * Gets a copy of the pending data to inject into the clipboard. + * If no `format` is provided, a copy of ALL pending data formats will be returned. + * + * @returns `String` or `Object` + */ + ZeroClipboard.prototype.getData = function() { + if (!_clientMeta[this.id]) { + throw new Error("Attempted to get pending clipboard data from a destroyed ZeroClipboard client instance"); + } + return ZeroClipboard.getData.apply(this, _args(arguments)); + }; + if (typeof define === "function" && define.amd) { + define(function() { + return ZeroClipboard; + }); + } else if (typeof module === "object" && module && typeof module.exports === "object" && module.exports) { + module.exports = ZeroClipboard; + } else { + window.ZeroClipboard = ZeroClipboard; + } +})(function() { + return this || window; +}()); \ No newline at end of file diff --git a/static/src/js/lib/bootstrap/bootstrap.js b/static/src/js/lib/bootstrap/bootstrap.js index 5debfd7..fb914ec 100644 --- a/static/src/js/lib/bootstrap/bootstrap.js +++ b/static/src/js/lib/bootstrap/bootstrap.js @@ -1,2363 +1 @@ -/*! - * Bootstrap v3.3.5 (http://getbootstrap.com) - * Copyright 2011-2015 Twitter, Inc. - * Licensed under the MIT license - */ - -if (typeof jQuery === 'undefined') { - throw new Error('Bootstrap\'s JavaScript requires jQuery') -} - -+function ($) { - 'use strict'; - var version = $.fn.jquery.split(' ')[0].split('.') - if ((version[0] < 2 && version[1] < 9) || (version[0] == 1 && version[1] == 9 && version[2] < 1)) { - throw new Error('Bootstrap\'s JavaScript requires jQuery version 1.9.1 or higher') - } -}(jQuery); - -/* ======================================================================== - * Bootstrap: transition.js v3.3.5 - * http://getbootstrap.com/javascript/#transitions - * ======================================================================== - * Copyright 2011-2015 Twitter, Inc. - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) - * ======================================================================== */ - - -+function ($) { - 'use strict'; - - // CSS TRANSITION SUPPORT (Shoutout: http://www.modernizr.com/) - // ============================================================ - - function transitionEnd() { - var el = document.createElement('bootstrap') - - var transEndEventNames = { - WebkitTransition : 'webkitTransitionEnd', - MozTransition : 'transitionend', - OTransition : 'oTransitionEnd otransitionend', - transition : 'transitionend' - } - - for (var name in transEndEventNames) { - if (el.style[name] !== undefined) { - return { end: transEndEventNames[name] } - } - } - - return false // explicit for ie8 ( ._.) - } - - // http://blog.alexmaccaw.com/css-transitions - $.fn.emulateTransitionEnd = function (duration) { - var called = false - var $el = this - $(this).one('bsTransitionEnd', function () { called = true }) - var callback = function () { if (!called) $($el).trigger($.support.transition.end) } - setTimeout(callback, duration) - return this - } - - $(function () { - $.support.transition = transitionEnd() - - if (!$.support.transition) return - - $.event.special.bsTransitionEnd = { - bindType: $.support.transition.end, - delegateType: $.support.transition.end, - handle: function (e) { - if ($(e.target).is(this)) return e.handleObj.handler.apply(this, arguments) - } - } - }) - -}(jQuery); - -/* ======================================================================== - * Bootstrap: alert.js v3.3.5 - * http://getbootstrap.com/javascript/#alerts - * ======================================================================== - * Copyright 2011-2015 Twitter, Inc. - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) - * ======================================================================== */ - - -+function ($) { - 'use strict'; - - // ALERT CLASS DEFINITION - // ====================== - - var dismiss = '[data-dismiss="alert"]' - var Alert = function (el) { - $(el).on('click', dismiss, this.close) - } - - Alert.VERSION = '3.3.5' - - Alert.TRANSITION_DURATION = 150 - - Alert.prototype.close = function (e) { - var $this = $(this) - var selector = $this.attr('data-target') - - if (!selector) { - selector = $this.attr('href') - selector = selector && selector.replace(/.*(?=#[^\s]*$)/, '') // strip for ie7 - } - - var $parent = $(selector) - - if (e) e.preventDefault() - - if (!$parent.length) { - $parent = $this.closest('.alert') - } - - $parent.trigger(e = $.Event('close.bs.alert')) - - if (e.isDefaultPrevented()) return - - $parent.removeClass('in') - - function removeElement() { - // detach from parent, fire event then clean up data - $parent.detach().trigger('closed.bs.alert').remove() - } - - $.support.transition && $parent.hasClass('fade') ? - $parent - .one('bsTransitionEnd', removeElement) - .emulateTransitionEnd(Alert.TRANSITION_DURATION) : - removeElement() - } - - - // ALERT PLUGIN DEFINITION - // ======================= - - function Plugin(option) { - return this.each(function () { - var $this = $(this) - var data = $this.data('bs.alert') - - if (!data) $this.data('bs.alert', (data = new Alert(this))) - if (typeof option == 'string') data[option].call($this) - }) - } - - var old = $.fn.alert - - $.fn.alert = Plugin - $.fn.alert.Constructor = Alert - - - // ALERT NO CONFLICT - // ================= - - $.fn.alert.noConflict = function () { - $.fn.alert = old - return this - } - - - // ALERT DATA-API - // ============== - - $(document).on('click.bs.alert.data-api', dismiss, Alert.prototype.close) - -}(jQuery); - -/* ======================================================================== - * Bootstrap: button.js v3.3.5 - * http://getbootstrap.com/javascript/#buttons - * ======================================================================== - * Copyright 2011-2015 Twitter, Inc. - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) - * ======================================================================== */ - - -+function ($) { - 'use strict'; - - // BUTTON PUBLIC CLASS DEFINITION - // ============================== - - var Button = function (element, options) { - this.$element = $(element) - this.options = $.extend({}, Button.DEFAULTS, options) - this.isLoading = false - } - - Button.VERSION = '3.3.5' - - Button.DEFAULTS = { - loadingText: 'loading...' - } - - Button.prototype.setState = function (state) { - var d = 'disabled' - var $el = this.$element - var val = $el.is('input') ? 'val' : 'html' - var data = $el.data() - - state += 'Text' - - if (data.resetText == null) $el.data('resetText', $el[val]()) - - // push to event loop to allow forms to submit - setTimeout($.proxy(function () { - $el[val](data[state] == null ? this.options[state] : data[state]) - - if (state == 'loadingText') { - this.isLoading = true - $el.addClass(d).attr(d, d) - } else if (this.isLoading) { - this.isLoading = false - $el.removeClass(d).removeAttr(d) - } - }, this), 0) - } - - Button.prototype.toggle = function () { - var changed = true - var $parent = this.$element.closest('[data-toggle="buttons"]') - - if ($parent.length) { - var $input = this.$element.find('input') - if ($input.prop('type') == 'radio') { - if ($input.prop('checked')) changed = false - $parent.find('.active').removeClass('active') - this.$element.addClass('active') - } else if ($input.prop('type') == 'checkbox') { - if (($input.prop('checked')) !== this.$element.hasClass('active')) changed = false - this.$element.toggleClass('active') - } - $input.prop('checked', this.$element.hasClass('active')) - if (changed) $input.trigger('change') - } else { - this.$element.attr('aria-pressed', !this.$element.hasClass('active')) - this.$element.toggleClass('active') - } - } - - - // BUTTON PLUGIN DEFINITION - // ======================== - - function Plugin(option) { - return this.each(function () { - var $this = $(this) - var data = $this.data('bs.button') - var options = typeof option == 'object' && option - - if (!data) $this.data('bs.button', (data = new Button(this, options))) - - if (option == 'toggle') data.toggle() - else if (option) data.setState(option) - }) - } - - var old = $.fn.button - - $.fn.button = Plugin - $.fn.button.Constructor = Button - - - // BUTTON NO CONFLICT - // ================== - - $.fn.button.noConflict = function () { - $.fn.button = old - return this - } - - - // BUTTON DATA-API - // =============== - - $(document) - .on('click.bs.button.data-api', '[data-toggle^="button"]', function (e) { - var $btn = $(e.target) - if (!$btn.hasClass('btn')) $btn = $btn.closest('.btn') - Plugin.call($btn, 'toggle') - if (!($(e.target).is('input[type="radio"]') || $(e.target).is('input[type="checkbox"]'))) e.preventDefault() - }) - .on('focus.bs.button.data-api blur.bs.button.data-api', '[data-toggle^="button"]', function (e) { - $(e.target).closest('.btn').toggleClass('focus', /^focus(in)?$/.test(e.type)) - }) - -}(jQuery); - -/* ======================================================================== - * Bootstrap: carousel.js v3.3.5 - * http://getbootstrap.com/javascript/#carousel - * ======================================================================== - * Copyright 2011-2015 Twitter, Inc. - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) - * ======================================================================== */ - - -+function ($) { - 'use strict'; - - // CAROUSEL CLASS DEFINITION - // ========================= - - var Carousel = function (element, options) { - this.$element = $(element) - this.$indicators = this.$element.find('.carousel-indicators') - this.options = options - this.paused = null - this.sliding = null - this.interval = null - this.$active = null - this.$items = null - - this.options.keyboard && this.$element.on('keydown.bs.carousel', $.proxy(this.keydown, this)) - - this.options.pause == 'hover' && !('ontouchstart' in document.documentElement) && this.$element - .on('mouseenter.bs.carousel', $.proxy(this.pause, this)) - .on('mouseleave.bs.carousel', $.proxy(this.cycle, this)) - } - - Carousel.VERSION = '3.3.5' - - Carousel.TRANSITION_DURATION = 600 - - Carousel.DEFAULTS = { - interval: 5000, - pause: 'hover', - wrap: true, - keyboard: true - } - - Carousel.prototype.keydown = function (e) { - if (/input|textarea/i.test(e.target.tagName)) return - switch (e.which) { - case 37: this.prev(); break - case 39: this.next(); break - default: return - } - - e.preventDefault() - } - - Carousel.prototype.cycle = function (e) { - e || (this.paused = false) - - this.interval && clearInterval(this.interval) - - this.options.interval - && !this.paused - && (this.interval = setInterval($.proxy(this.next, this), this.options.interval)) - - return this - } - - Carousel.prototype.getItemIndex = function (item) { - this.$items = item.parent().children('.item') - return this.$items.index(item || this.$active) - } - - Carousel.prototype.getItemForDirection = function (direction, active) { - var activeIndex = this.getItemIndex(active) - var willWrap = (direction == 'prev' && activeIndex === 0) - || (direction == 'next' && activeIndex == (this.$items.length - 1)) - if (willWrap && !this.options.wrap) return active - var delta = direction == 'prev' ? -1 : 1 - var itemIndex = (activeIndex + delta) % this.$items.length - return this.$items.eq(itemIndex) - } - - Carousel.prototype.to = function (pos) { - var that = this - var activeIndex = this.getItemIndex(this.$active = this.$element.find('.item.active')) - - if (pos > (this.$items.length - 1) || pos < 0) return - - if (this.sliding) return this.$element.one('slid.bs.carousel', function () { that.to(pos) }) // yes, "slid" - if (activeIndex == pos) return this.pause().cycle() - - return this.slide(pos > activeIndex ? 'next' : 'prev', this.$items.eq(pos)) - } - - Carousel.prototype.pause = function (e) { - e || (this.paused = true) - - if (this.$element.find('.next, .prev').length && $.support.transition) { - this.$element.trigger($.support.transition.end) - this.cycle(true) - } - - this.interval = clearInterval(this.interval) - - return this - } - - Carousel.prototype.next = function () { - if (this.sliding) return - return this.slide('next') - } - - Carousel.prototype.prev = function () { - if (this.sliding) return - return this.slide('prev') - } - - Carousel.prototype.slide = function (type, next) { - var $active = this.$element.find('.item.active') - var $next = next || this.getItemForDirection(type, $active) - var isCycling = this.interval - var direction = type == 'next' ? 'left' : 'right' - var that = this - - if ($next.hasClass('active')) return (this.sliding = false) - - var relatedTarget = $next[0] - var slideEvent = $.Event('slide.bs.carousel', { - relatedTarget: relatedTarget, - direction: direction - }) - this.$element.trigger(slideEvent) - if (slideEvent.isDefaultPrevented()) return - - this.sliding = true - - isCycling && this.pause() - - if (this.$indicators.length) { - this.$indicators.find('.active').removeClass('active') - var $nextIndicator = $(this.$indicators.children()[this.getItemIndex($next)]) - $nextIndicator && $nextIndicator.addClass('active') - } - - var slidEvent = $.Event('slid.bs.carousel', { relatedTarget: relatedTarget, direction: direction }) // yes, "slid" - if ($.support.transition && this.$element.hasClass('slide')) { - $next.addClass(type) - $next[0].offsetWidth // force reflow - $active.addClass(direction) - $next.addClass(direction) - $active - .one('bsTransitionEnd', function () { - $next.removeClass([type, direction].join(' ')).addClass('active') - $active.removeClass(['active', direction].join(' ')) - that.sliding = false - setTimeout(function () { - that.$element.trigger(slidEvent) - }, 0) - }) - .emulateTransitionEnd(Carousel.TRANSITION_DURATION) - } else { - $active.removeClass('active') - $next.addClass('active') - this.sliding = false - this.$element.trigger(slidEvent) - } - - isCycling && this.cycle() - - return this - } - - - // CAROUSEL PLUGIN DEFINITION - // ========================== - - function Plugin(option) { - return this.each(function () { - var $this = $(this) - var data = $this.data('bs.carousel') - var options = $.extend({}, Carousel.DEFAULTS, $this.data(), typeof option == 'object' && option) - var action = typeof option == 'string' ? option : options.slide - - if (!data) $this.data('bs.carousel', (data = new Carousel(this, options))) - if (typeof option == 'number') data.to(option) - else if (action) data[action]() - else if (options.interval) data.pause().cycle() - }) - } - - var old = $.fn.carousel - - $.fn.carousel = Plugin - $.fn.carousel.Constructor = Carousel - - - // CAROUSEL NO CONFLICT - // ==================== - - $.fn.carousel.noConflict = function () { - $.fn.carousel = old - return this - } - - - // CAROUSEL DATA-API - // ================= - - var clickHandler = function (e) { - var href - var $this = $(this) - var $target = $($this.attr('data-target') || (href = $this.attr('href')) && href.replace(/.*(?=#[^\s]+$)/, '')) // strip for ie7 - if (!$target.hasClass('carousel')) return - var options = $.extend({}, $target.data(), $this.data()) - var slideIndex = $this.attr('data-slide-to') - if (slideIndex) options.interval = false - - Plugin.call($target, options) - - if (slideIndex) { - $target.data('bs.carousel').to(slideIndex) - } - - e.preventDefault() - } - - $(document) - .on('click.bs.carousel.data-api', '[data-slide]', clickHandler) - .on('click.bs.carousel.data-api', '[data-slide-to]', clickHandler) - - $(window).on('load', function () { - $('[data-ride="carousel"]').each(function () { - var $carousel = $(this) - Plugin.call($carousel, $carousel.data()) - }) - }) - -}(jQuery); - -/* ======================================================================== - * Bootstrap: collapse.js v3.3.5 - * http://getbootstrap.com/javascript/#collapse - * ======================================================================== - * Copyright 2011-2015 Twitter, Inc. - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) - * ======================================================================== */ - - -+function ($) { - 'use strict'; - - // COLLAPSE PUBLIC CLASS DEFINITION - // ================================ - - var Collapse = function (element, options) { - this.$element = $(element) - this.options = $.extend({}, Collapse.DEFAULTS, options) - this.$trigger = $('[data-toggle="collapse"][href="#' + element.id + '"],' + - '[data-toggle="collapse"][data-target="#' + element.id + '"]') - this.transitioning = null - - if (this.options.parent) { - this.$parent = this.getParent() - } else { - this.addAriaAndCollapsedClass(this.$element, this.$trigger) - } - - if (this.options.toggle) this.toggle() - } - - Collapse.VERSION = '3.3.5' - - Collapse.TRANSITION_DURATION = 350 - - Collapse.DEFAULTS = { - toggle: true - } - - Collapse.prototype.dimension = function () { - var hasWidth = this.$element.hasClass('width') - return hasWidth ? 'width' : 'height' - } - - Collapse.prototype.show = function () { - if (this.transitioning || this.$element.hasClass('in')) return - - var activesData - var actives = this.$parent && this.$parent.children('.panel').children('.in, .collapsing') - - if (actives && actives.length) { - activesData = actives.data('bs.collapse') - if (activesData && activesData.transitioning) return - } - - var startEvent = $.Event('show.bs.collapse') - this.$element.trigger(startEvent) - if (startEvent.isDefaultPrevented()) return - - if (actives && actives.length) { - Plugin.call(actives, 'hide') - activesData || actives.data('bs.collapse', null) - } - - var dimension = this.dimension() - - this.$element - .removeClass('collapse') - .addClass('collapsing')[dimension](0) - .attr('aria-expanded', true) - - this.$trigger - .removeClass('collapsed') - .attr('aria-expanded', true) - - this.transitioning = 1 - - var complete = function () { - this.$element - .removeClass('collapsing') - .addClass('collapse in')[dimension]('') - this.transitioning = 0 - this.$element - .trigger('shown.bs.collapse') - } - - if (!$.support.transition) return complete.call(this) - - var scrollSize = $.camelCase(['scroll', dimension].join('-')) - - this.$element - .one('bsTransitionEnd', $.proxy(complete, this)) - .emulateTransitionEnd(Collapse.TRANSITION_DURATION)[dimension](this.$element[0][scrollSize]) - } - - Collapse.prototype.hide = function () { - if (this.transitioning || !this.$element.hasClass('in')) return - - var startEvent = $.Event('hide.bs.collapse') - this.$element.trigger(startEvent) - if (startEvent.isDefaultPrevented()) return - - var dimension = this.dimension() - - this.$element[dimension](this.$element[dimension]())[0].offsetHeight - - this.$element - .addClass('collapsing') - .removeClass('collapse in') - .attr('aria-expanded', false) - - this.$trigger - .addClass('collapsed') - .attr('aria-expanded', false) - - this.transitioning = 1 - - var complete = function () { - this.transitioning = 0 - this.$element - .removeClass('collapsing') - .addClass('collapse') - .trigger('hidden.bs.collapse') - } - - if (!$.support.transition) return complete.call(this) - - this.$element - [dimension](0) - .one('bsTransitionEnd', $.proxy(complete, this)) - .emulateTransitionEnd(Collapse.TRANSITION_DURATION) - } - - Collapse.prototype.toggle = function () { - this[this.$element.hasClass('in') ? 'hide' : 'show']() - } - - Collapse.prototype.getParent = function () { - return $(this.options.parent) - .find('[data-toggle="collapse"][data-parent="' + this.options.parent + '"]') - .each($.proxy(function (i, element) { - var $element = $(element) - this.addAriaAndCollapsedClass(getTargetFromTrigger($element), $element) - }, this)) - .end() - } - - Collapse.prototype.addAriaAndCollapsedClass = function ($element, $trigger) { - var isOpen = $element.hasClass('in') - - $element.attr('aria-expanded', isOpen) - $trigger - .toggleClass('collapsed', !isOpen) - .attr('aria-expanded', isOpen) - } - - function getTargetFromTrigger($trigger) { - var href - var target = $trigger.attr('data-target') - || (href = $trigger.attr('href')) && href.replace(/.*(?=#[^\s]+$)/, '') // strip for ie7 - - return $(target) - } - - - // COLLAPSE PLUGIN DEFINITION - // ========================== - - function Plugin(option) { - return this.each(function () { - var $this = $(this) - var data = $this.data('bs.collapse') - var options = $.extend({}, Collapse.DEFAULTS, $this.data(), typeof option == 'object' && option) - - if (!data && options.toggle && /show|hide/.test(option)) options.toggle = false - if (!data) $this.data('bs.collapse', (data = new Collapse(this, options))) - if (typeof option == 'string') data[option]() - }) - } - - var old = $.fn.collapse - - $.fn.collapse = Plugin - $.fn.collapse.Constructor = Collapse - - - // COLLAPSE NO CONFLICT - // ==================== - - $.fn.collapse.noConflict = function () { - $.fn.collapse = old - return this - } - - - // COLLAPSE DATA-API - // ================= - - $(document).on('click.bs.collapse.data-api', '[data-toggle="collapse"]', function (e) { - var $this = $(this) - - if (!$this.attr('data-target')) e.preventDefault() - - var $target = getTargetFromTrigger($this) - var data = $target.data('bs.collapse') - var option = data ? 'toggle' : $this.data() - - Plugin.call($target, option) - }) - -}(jQuery); - -/* ======================================================================== - * Bootstrap: dropdown.js v3.3.5 - * http://getbootstrap.com/javascript/#dropdowns - * ======================================================================== - * Copyright 2011-2015 Twitter, Inc. - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) - * ======================================================================== */ - - -+function ($) { - 'use strict'; - - // DROPDOWN CLASS DEFINITION - // ========================= - - var backdrop = '.dropdown-backdrop' - var toggle = '[data-toggle="dropdown"]' - var Dropdown = function (element) { - $(element).on('click.bs.dropdown', this.toggle) - } - - Dropdown.VERSION = '3.3.5' - - function getParent($this) { - var selector = $this.attr('data-target') - - if (!selector) { - selector = $this.attr('href') - selector = selector && /#[A-Za-z]/.test(selector) && selector.replace(/.*(?=#[^\s]*$)/, '') // strip for ie7 - } - - var $parent = selector && $(selector) - - return $parent && $parent.length ? $parent : $this.parent() - } - - function clearMenus(e) { - if (e && e.which === 3) return - $(backdrop).remove() - $(toggle).each(function () { - var $this = $(this) - var $parent = getParent($this) - var relatedTarget = { relatedTarget: this } - - if (!$parent.hasClass('open')) return - - if (e && e.type == 'click' && /input|textarea/i.test(e.target.tagName) && $.contains($parent[0], e.target)) return - - $parent.trigger(e = $.Event('hide.bs.dropdown', relatedTarget)) - - if (e.isDefaultPrevented()) return - - $this.attr('aria-expanded', 'false') - $parent.removeClass('open').trigger('hidden.bs.dropdown', relatedTarget) - }) - } - - Dropdown.prototype.toggle = function (e) { - var $this = $(this) - - if ($this.is('.disabled, :disabled')) return - - var $parent = getParent($this) - var isActive = $parent.hasClass('open') - - clearMenus() - - if (!isActive) { - if ('ontouchstart' in document.documentElement && !$parent.closest('.navbar-nav').length) { - // if mobile we use a backdrop because click events don't delegate - $(document.createElement('div')) - .addClass('dropdown-backdrop') - .insertAfter($(this)) - .on('click', clearMenus) - } - - var relatedTarget = { relatedTarget: this } - $parent.trigger(e = $.Event('show.bs.dropdown', relatedTarget)) - - if (e.isDefaultPrevented()) return - - $this - .trigger('focus') - .attr('aria-expanded', 'true') - - $parent - .toggleClass('open') - .trigger('shown.bs.dropdown', relatedTarget) - } - - return false - } - - Dropdown.prototype.keydown = function (e) { - if (!/(38|40|27|32)/.test(e.which) || /input|textarea/i.test(e.target.tagName)) return - - var $this = $(this) - - e.preventDefault() - e.stopPropagation() - - if ($this.is('.disabled, :disabled')) return - - var $parent = getParent($this) - var isActive = $parent.hasClass('open') - - if (!isActive && e.which != 27 || isActive && e.which == 27) { - if (e.which == 27) $parent.find(toggle).trigger('focus') - return $this.trigger('click') - } - - var desc = ' li:not(.disabled):visible a' - var $items = $parent.find('.dropdown-menu' + desc) - - if (!$items.length) return - - var index = $items.index(e.target) - - if (e.which == 38 && index > 0) index-- // up - if (e.which == 40 && index < $items.length - 1) index++ // down - if (!~index) index = 0 - - $items.eq(index).trigger('focus') - } - - - // DROPDOWN PLUGIN DEFINITION - // ========================== - - function Plugin(option) { - return this.each(function () { - var $this = $(this) - var data = $this.data('bs.dropdown') - - if (!data) $this.data('bs.dropdown', (data = new Dropdown(this))) - if (typeof option == 'string') data[option].call($this) - }) - } - - var old = $.fn.dropdown - - $.fn.dropdown = Plugin - $.fn.dropdown.Constructor = Dropdown - - - // DROPDOWN NO CONFLICT - // ==================== - - $.fn.dropdown.noConflict = function () { - $.fn.dropdown = old - return this - } - - - // APPLY TO STANDARD DROPDOWN ELEMENTS - // =================================== - - $(document) - .on('click.bs.dropdown.data-api', clearMenus) - .on('click.bs.dropdown.data-api', '.dropdown form', function (e) { e.stopPropagation() }) - .on('click.bs.dropdown.data-api', toggle, Dropdown.prototype.toggle) - .on('keydown.bs.dropdown.data-api', toggle, Dropdown.prototype.keydown) - .on('keydown.bs.dropdown.data-api', '.dropdown-menu', Dropdown.prototype.keydown) - -}(jQuery); - -/* ======================================================================== - * Bootstrap: modal.js v3.3.5 - * http://getbootstrap.com/javascript/#modals - * ======================================================================== - * Copyright 2011-2015 Twitter, Inc. - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) - * ======================================================================== */ - - -+function ($) { - 'use strict'; - - // MODAL CLASS DEFINITION - // ====================== - - var Modal = function (element, options) { - this.options = options - this.$body = $(document.body) - this.$element = $(element) - this.$dialog = this.$element.find('.modal-dialog') - this.$backdrop = null - this.isShown = null - this.originalBodyPad = null - this.scrollbarWidth = 0 - this.ignoreBackdropClick = false - - if (this.options.remote) { - this.$element - .find('.modal-content') - .load(this.options.remote, $.proxy(function () { - this.$element.trigger('loaded.bs.modal') - }, this)) - } - } - - Modal.VERSION = '3.3.5' - - Modal.TRANSITION_DURATION = 300 - Modal.BACKDROP_TRANSITION_DURATION = 150 - - Modal.DEFAULTS = { - backdrop: true, - keyboard: true, - show: true - } - - Modal.prototype.toggle = function (_relatedTarget) { - return this.isShown ? this.hide() : this.show(_relatedTarget) - } - - Modal.prototype.show = function (_relatedTarget) { - var that = this - var e = $.Event('show.bs.modal', { relatedTarget: _relatedTarget }) - - this.$element.trigger(e) - - if (this.isShown || e.isDefaultPrevented()) return - - this.isShown = true - - this.checkScrollbar() - this.setScrollbar() - this.$body.addClass('modal-open') - - this.escape() - this.resize() - - this.$element.on('click.dismiss.bs.modal', '[data-dismiss="modal"]', $.proxy(this.hide, this)) - - this.$dialog.on('mousedown.dismiss.bs.modal', function () { - that.$element.one('mouseup.dismiss.bs.modal', function (e) { - if ($(e.target).is(that.$element)) that.ignoreBackdropClick = true - }) - }) - - this.backdrop(function () { - var transition = $.support.transition && that.$element.hasClass('fade') - - if (!that.$element.parent().length) { - that.$element.appendTo(that.$body) // don't move modals dom position - } - - that.$element - .show() - .scrollTop(0) - - that.adjustDialog() - - if (transition) { - that.$element[0].offsetWidth // force reflow - } - - that.$element.addClass('in') - - that.enforceFocus() - - var e = $.Event('shown.bs.modal', { relatedTarget: _relatedTarget }) - - transition ? - that.$dialog // wait for modal to slide in - .one('bsTransitionEnd', function () { - that.$element.trigger('focus').trigger(e) - }) - .emulateTransitionEnd(Modal.TRANSITION_DURATION) : - that.$element.trigger('focus').trigger(e) - }) - } - - Modal.prototype.hide = function (e) { - if (e) e.preventDefault() - - e = $.Event('hide.bs.modal') - - this.$element.trigger(e) - - if (!this.isShown || e.isDefaultPrevented()) return - - this.isShown = false - - this.escape() - this.resize() - - $(document).off('focusin.bs.modal') - - this.$element - .removeClass('in') - .off('click.dismiss.bs.modal') - .off('mouseup.dismiss.bs.modal') - - this.$dialog.off('mousedown.dismiss.bs.modal') - - $.support.transition && this.$element.hasClass('fade') ? - this.$element - .one('bsTransitionEnd', $.proxy(this.hideModal, this)) - .emulateTransitionEnd(Modal.TRANSITION_DURATION) : - this.hideModal() - } - - Modal.prototype.enforceFocus = function () { - $(document) - .off('focusin.bs.modal') // guard against infinite focus loop - .on('focusin.bs.modal', $.proxy(function (e) { - if (this.$element[0] !== e.target && !this.$element.has(e.target).length) { - this.$element.trigger('focus') - } - }, this)) - } - - Modal.prototype.escape = function () { - if (this.isShown && this.options.keyboard) { - this.$element.on('keydown.dismiss.bs.modal', $.proxy(function (e) { - e.which == 27 && this.hide() - }, this)) - } else if (!this.isShown) { - this.$element.off('keydown.dismiss.bs.modal') - } - } - - Modal.prototype.resize = function () { - if (this.isShown) { - $(window).on('resize.bs.modal', $.proxy(this.handleUpdate, this)) - } else { - $(window).off('resize.bs.modal') - } - } - - Modal.prototype.hideModal = function () { - var that = this - this.$element.hide() - this.backdrop(function () { - that.$body.removeClass('modal-open') - that.resetAdjustments() - that.resetScrollbar() - that.$element.trigger('hidden.bs.modal') - }) - } - - Modal.prototype.removeBackdrop = function () { - this.$backdrop && this.$backdrop.remove() - this.$backdrop = null - } - - Modal.prototype.backdrop = function (callback) { - var that = this - var animate = this.$element.hasClass('fade') ? 'fade' : '' - - if (this.isShown && this.options.backdrop) { - var doAnimate = $.support.transition && animate - - this.$backdrop = $(document.createElement('div')) - .addClass('modal-backdrop ' + animate) - .appendTo(this.$body) - - this.$element.on('click.dismiss.bs.modal', $.proxy(function (e) { - if (this.ignoreBackdropClick) { - this.ignoreBackdropClick = false - return - } - if (e.target !== e.currentTarget) return - this.options.backdrop == 'static' - ? this.$element[0].focus() - : this.hide() - }, this)) - - if (doAnimate) this.$backdrop[0].offsetWidth // force reflow - - this.$backdrop.addClass('in') - - if (!callback) return - - doAnimate ? - this.$backdrop - .one('bsTransitionEnd', callback) - .emulateTransitionEnd(Modal.BACKDROP_TRANSITION_DURATION) : - callback() - - } else if (!this.isShown && this.$backdrop) { - this.$backdrop.removeClass('in') - - var callbackRemove = function () { - that.removeBackdrop() - callback && callback() - } - $.support.transition && this.$element.hasClass('fade') ? - this.$backdrop - .one('bsTransitionEnd', callbackRemove) - .emulateTransitionEnd(Modal.BACKDROP_TRANSITION_DURATION) : - callbackRemove() - - } else if (callback) { - callback() - } - } - - // these following methods are used to handle overflowing modals - - Modal.prototype.handleUpdate = function () { - this.adjustDialog() - } - - Modal.prototype.adjustDialog = function () { - var modalIsOverflowing = this.$element[0].scrollHeight > document.documentElement.clientHeight - - this.$element.css({ - paddingLeft: !this.bodyIsOverflowing && modalIsOverflowing ? this.scrollbarWidth : '', - paddingRight: this.bodyIsOverflowing && !modalIsOverflowing ? this.scrollbarWidth : '' - }) - } - - Modal.prototype.resetAdjustments = function () { - this.$element.css({ - paddingLeft: '', - paddingRight: '' - }) - } - - Modal.prototype.checkScrollbar = function () { - var fullWindowWidth = window.innerWidth - if (!fullWindowWidth) { // workaround for missing window.innerWidth in IE8 - var documentElementRect = document.documentElement.getBoundingClientRect() - fullWindowWidth = documentElementRect.right - Math.abs(documentElementRect.left) - } - this.bodyIsOverflowing = document.body.clientWidth < fullWindowWidth - this.scrollbarWidth = this.measureScrollbar() - } - - Modal.prototype.setScrollbar = function () { - var bodyPad = parseInt((this.$body.css('padding-right') || 0), 10) - this.originalBodyPad = document.body.style.paddingRight || '' - if (this.bodyIsOverflowing) this.$body.css('padding-right', bodyPad + this.scrollbarWidth) - } - - Modal.prototype.resetScrollbar = function () { - this.$body.css('padding-right', this.originalBodyPad) - } - - Modal.prototype.measureScrollbar = function () { // thx walsh - var scrollDiv = document.createElement('div') - scrollDiv.className = 'modal-scrollbar-measure' - this.$body.append(scrollDiv) - var scrollbarWidth = scrollDiv.offsetWidth - scrollDiv.clientWidth - this.$body[0].removeChild(scrollDiv) - return scrollbarWidth - } - - - // MODAL PLUGIN DEFINITION - // ======================= - - function Plugin(option, _relatedTarget) { - return this.each(function () { - var $this = $(this) - var data = $this.data('bs.modal') - var options = $.extend({}, Modal.DEFAULTS, $this.data(), typeof option == 'object' && option) - - if (!data) $this.data('bs.modal', (data = new Modal(this, options))) - if (typeof option == 'string') data[option](_relatedTarget) - else if (options.show) data.show(_relatedTarget) - }) - } - - var old = $.fn.modal - - $.fn.modal = Plugin - $.fn.modal.Constructor = Modal - - - // MODAL NO CONFLICT - // ================= - - $.fn.modal.noConflict = function () { - $.fn.modal = old - return this - } - - - // MODAL DATA-API - // ============== - - $(document).on('click.bs.modal.data-api', '[data-toggle="modal"]', function (e) { - var $this = $(this) - var href = $this.attr('href') - var $target = $($this.attr('data-target') || (href && href.replace(/.*(?=#[^\s]+$)/, ''))) // strip for ie7 - var option = $target.data('bs.modal') ? 'toggle' : $.extend({ remote: !/#/.test(href) && href }, $target.data(), $this.data()) - - if ($this.is('a')) e.preventDefault() - - $target.one('show.bs.modal', function (showEvent) { - if (showEvent.isDefaultPrevented()) return // only register focus restorer if modal will actually get shown - $target.one('hidden.bs.modal', function () { - $this.is(':visible') && $this.trigger('focus') - }) - }) - Plugin.call($target, option, this) - }) - -}(jQuery); - -/* ======================================================================== - * Bootstrap: tooltip.js v3.3.5 - * http://getbootstrap.com/javascript/#tooltip - * Inspired by the original jQuery.tipsy by Jason Frame - * ======================================================================== - * Copyright 2011-2015 Twitter, Inc. - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) - * ======================================================================== */ - - -+function ($) { - 'use strict'; - - // TOOLTIP PUBLIC CLASS DEFINITION - // =============================== - - var Tooltip = function (element, options) { - this.type = null - this.options = null - this.enabled = null - this.timeout = null - this.hoverState = null - this.$element = null - this.inState = null - - this.init('tooltip', element, options) - } - - Tooltip.VERSION = '3.3.5' - - Tooltip.TRANSITION_DURATION = 150 - - Tooltip.DEFAULTS = { - animation: true, - placement: 'top', - selector: false, - template: '', - trigger: 'hover focus', - title: '', - delay: 0, - html: false, - container: false, - viewport: { - selector: 'body', - padding: 0 - } - } - - Tooltip.prototype.init = function (type, element, options) { - this.enabled = true - this.type = type - this.$element = $(element) - this.options = this.getOptions(options) - this.$viewport = this.options.viewport && $($.isFunction(this.options.viewport) ? this.options.viewport.call(this, this.$element) : (this.options.viewport.selector || this.options.viewport)) - this.inState = { click: false, hover: false, focus: false } - - if (this.$element[0] instanceof document.constructor && !this.options.selector) { - throw new Error('`selector` option must be specified when initializing ' + this.type + ' on the window.document object!') - } - - var triggers = this.options.trigger.split(' ') - - for (var i = triggers.length; i--;) { - var trigger = triggers[i] - - if (trigger == 'click') { - this.$element.on('click.' + this.type, this.options.selector, $.proxy(this.toggle, this)) - } else if (trigger != 'manual') { - var eventIn = trigger == 'hover' ? 'mouseenter' : 'focusin' - var eventOut = trigger == 'hover' ? 'mouseleave' : 'focusout' - - this.$element.on(eventIn + '.' + this.type, this.options.selector, $.proxy(this.enter, this)) - this.$element.on(eventOut + '.' + this.type, this.options.selector, $.proxy(this.leave, this)) - } - } - - this.options.selector ? - (this._options = $.extend({}, this.options, { trigger: 'manual', selector: '' })) : - this.fixTitle() - } - - Tooltip.prototype.getDefaults = function () { - return Tooltip.DEFAULTS - } - - Tooltip.prototype.getOptions = function (options) { - options = $.extend({}, this.getDefaults(), this.$element.data(), options) - - if (options.delay && typeof options.delay == 'number') { - options.delay = { - show: options.delay, - hide: options.delay - } - } - - return options - } - - Tooltip.prototype.getDelegateOptions = function () { - var options = {} - var defaults = this.getDefaults() - - this._options && $.each(this._options, function (key, value) { - if (defaults[key] != value) options[key] = value - }) - - return options - } - - Tooltip.prototype.enter = function (obj) { - var self = obj instanceof this.constructor ? - obj : $(obj.currentTarget).data('bs.' + this.type) - - if (!self) { - self = new this.constructor(obj.currentTarget, this.getDelegateOptions()) - $(obj.currentTarget).data('bs.' + this.type, self) - } - - if (obj instanceof $.Event) { - self.inState[obj.type == 'focusin' ? 'focus' : 'hover'] = true - } - - if (self.tip().hasClass('in') || self.hoverState == 'in') { - self.hoverState = 'in' - return - } - - clearTimeout(self.timeout) - - self.hoverState = 'in' - - if (!self.options.delay || !self.options.delay.show) return self.show() - - self.timeout = setTimeout(function () { - if (self.hoverState == 'in') self.show() - }, self.options.delay.show) - } - - Tooltip.prototype.isInStateTrue = function () { - for (var key in this.inState) { - if (this.inState[key]) return true - } - - return false - } - - Tooltip.prototype.leave = function (obj) { - var self = obj instanceof this.constructor ? - obj : $(obj.currentTarget).data('bs.' + this.type) - - if (!self) { - self = new this.constructor(obj.currentTarget, this.getDelegateOptions()) - $(obj.currentTarget).data('bs.' + this.type, self) - } - - if (obj instanceof $.Event) { - self.inState[obj.type == 'focusout' ? 'focus' : 'hover'] = false - } - - if (self.isInStateTrue()) return - - clearTimeout(self.timeout) - - self.hoverState = 'out' - - if (!self.options.delay || !self.options.delay.hide) return self.hide() - - self.timeout = setTimeout(function () { - if (self.hoverState == 'out') self.hide() - }, self.options.delay.hide) - } - - Tooltip.prototype.show = function () { - var e = $.Event('show.bs.' + this.type) - - if (this.hasContent() && this.enabled) { - this.$element.trigger(e) - - var inDom = $.contains(this.$element[0].ownerDocument.documentElement, this.$element[0]) - if (e.isDefaultPrevented() || !inDom) return - var that = this - - var $tip = this.tip() - - var tipId = this.getUID(this.type) - - this.setContent() - $tip.attr('id', tipId) - this.$element.attr('aria-describedby', tipId) - - if (this.options.animation) $tip.addClass('fade') - - var placement = typeof this.options.placement == 'function' ? - this.options.placement.call(this, $tip[0], this.$element[0]) : - this.options.placement - - var autoToken = /\s?auto?\s?/i - var autoPlace = autoToken.test(placement) - if (autoPlace) placement = placement.replace(autoToken, '') || 'top' - - $tip - .detach() - .css({ top: 0, left: 0, display: 'block' }) - .addClass(placement) - .data('bs.' + this.type, this) - - this.options.container ? $tip.appendTo(this.options.container) : $tip.insertAfter(this.$element) - this.$element.trigger('inserted.bs.' + this.type) - - var pos = this.getPosition() - var actualWidth = $tip[0].offsetWidth - var actualHeight = $tip[0].offsetHeight - - if (autoPlace) { - var orgPlacement = placement - var viewportDim = this.getPosition(this.$viewport) - - placement = placement == 'bottom' && pos.bottom + actualHeight > viewportDim.bottom ? 'top' : - placement == 'top' && pos.top - actualHeight < viewportDim.top ? 'bottom' : - placement == 'right' && pos.right + actualWidth > viewportDim.width ? 'left' : - placement == 'left' && pos.left - actualWidth < viewportDim.left ? 'right' : - placement - - $tip - .removeClass(orgPlacement) - .addClass(placement) - } - - var calculatedOffset = this.getCalculatedOffset(placement, pos, actualWidth, actualHeight) - - this.applyPlacement(calculatedOffset, placement) - - var complete = function () { - var prevHoverState = that.hoverState - that.$element.trigger('shown.bs.' + that.type) - that.hoverState = null - - if (prevHoverState == 'out') that.leave(that) - } - - $.support.transition && this.$tip.hasClass('fade') ? - $tip - .one('bsTransitionEnd', complete) - .emulateTransitionEnd(Tooltip.TRANSITION_DURATION) : - complete() - } - } - - Tooltip.prototype.applyPlacement = function (offset, placement) { - var $tip = this.tip() - var width = $tip[0].offsetWidth - var height = $tip[0].offsetHeight - - // manually read margins because getBoundingClientRect includes difference - var marginTop = parseInt($tip.css('margin-top'), 10) - var marginLeft = parseInt($tip.css('margin-left'), 10) - - // we must check for NaN for ie 8/9 - if (isNaN(marginTop)) marginTop = 0 - if (isNaN(marginLeft)) marginLeft = 0 - - offset.top += marginTop - offset.left += marginLeft - - // $.fn.offset doesn't round pixel values - // so we use setOffset directly with our own function B-0 - $.offset.setOffset($tip[0], $.extend({ - using: function (props) { - $tip.css({ - top: Math.round(props.top), - left: Math.round(props.left) - }) - } - }, offset), 0) - - $tip.addClass('in') - - // check to see if placing tip in new offset caused the tip to resize itself - var actualWidth = $tip[0].offsetWidth - var actualHeight = $tip[0].offsetHeight - - if (placement == 'top' && actualHeight != height) { - offset.top = offset.top + height - actualHeight - } - - var delta = this.getViewportAdjustedDelta(placement, offset, actualWidth, actualHeight) - - if (delta.left) offset.left += delta.left - else offset.top += delta.top - - var isVertical = /top|bottom/.test(placement) - var arrowDelta = isVertical ? delta.left * 2 - width + actualWidth : delta.top * 2 - height + actualHeight - var arrowOffsetPosition = isVertical ? 'offsetWidth' : 'offsetHeight' - - $tip.offset(offset) - this.replaceArrow(arrowDelta, $tip[0][arrowOffsetPosition], isVertical) - } - - Tooltip.prototype.replaceArrow = function (delta, dimension, isVertical) { - this.arrow() - .css(isVertical ? 'left' : 'top', 50 * (1 - delta / dimension) + '%') - .css(isVertical ? 'top' : 'left', '') - } - - Tooltip.prototype.setContent = function () { - var $tip = this.tip() - var title = this.getTitle() - - $tip.find('.tooltip-inner')[this.options.html ? 'html' : 'text'](title) - $tip.removeClass('fade in top bottom left right') - } - - Tooltip.prototype.hide = function (callback) { - var that = this - var $tip = $(this.$tip) - var e = $.Event('hide.bs.' + this.type) - - function complete() { - if (that.hoverState != 'in') $tip.detach() - that.$element - .removeAttr('aria-describedby') - .trigger('hidden.bs.' + that.type) - callback && callback() - } - - this.$element.trigger(e) - - if (e.isDefaultPrevented()) return - - $tip.removeClass('in') - - $.support.transition && $tip.hasClass('fade') ? - $tip - .one('bsTransitionEnd', complete) - .emulateTransitionEnd(Tooltip.TRANSITION_DURATION) : - complete() - - this.hoverState = null - - return this - } - - Tooltip.prototype.fixTitle = function () { - var $e = this.$element - if ($e.attr('title') || typeof $e.attr('data-original-title') != 'string') { - $e.attr('data-original-title', $e.attr('title') || '').attr('title', '') - } - } - - Tooltip.prototype.hasContent = function () { - return this.getTitle() - } - - Tooltip.prototype.getPosition = function ($element) { - $element = $element || this.$element - - var el = $element[0] - var isBody = el.tagName == 'BODY' - - var elRect = el.getBoundingClientRect() - if (elRect.width == null) { - // width and height are missing in IE8, so compute them manually; see https://github.com/twbs/bootstrap/issues/14093 - elRect = $.extend({}, elRect, { width: elRect.right - elRect.left, height: elRect.bottom - elRect.top }) - } - var elOffset = isBody ? { top: 0, left: 0 } : $element.offset() - var scroll = { scroll: isBody ? document.documentElement.scrollTop || document.body.scrollTop : $element.scrollTop() } - var outerDims = isBody ? { width: $(window).width(), height: $(window).height() } : null - - return $.extend({}, elRect, scroll, outerDims, elOffset) - } - - Tooltip.prototype.getCalculatedOffset = function (placement, pos, actualWidth, actualHeight) { - return placement == 'bottom' ? { top: pos.top + pos.height, left: pos.left + pos.width / 2 - actualWidth / 2 } : - placement == 'top' ? { top: pos.top - actualHeight, left: pos.left + pos.width / 2 - actualWidth / 2 } : - placement == 'left' ? { top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left - actualWidth } : - /* placement == 'right' */ { top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left + pos.width } - - } - - Tooltip.prototype.getViewportAdjustedDelta = function (placement, pos, actualWidth, actualHeight) { - var delta = { top: 0, left: 0 } - if (!this.$viewport) return delta - - var viewportPadding = this.options.viewport && this.options.viewport.padding || 0 - var viewportDimensions = this.getPosition(this.$viewport) - - if (/right|left/.test(placement)) { - var topEdgeOffset = pos.top - viewportPadding - viewportDimensions.scroll - var bottomEdgeOffset = pos.top + viewportPadding - viewportDimensions.scroll + actualHeight - if (topEdgeOffset < viewportDimensions.top) { // top overflow - delta.top = viewportDimensions.top - topEdgeOffset - } else if (bottomEdgeOffset > viewportDimensions.top + viewportDimensions.height) { // bottom overflow - delta.top = viewportDimensions.top + viewportDimensions.height - bottomEdgeOffset - } - } else { - var leftEdgeOffset = pos.left - viewportPadding - var rightEdgeOffset = pos.left + viewportPadding + actualWidth - if (leftEdgeOffset < viewportDimensions.left) { // left overflow - delta.left = viewportDimensions.left - leftEdgeOffset - } else if (rightEdgeOffset > viewportDimensions.right) { // right overflow - delta.left = viewportDimensions.left + viewportDimensions.width - rightEdgeOffset - } - } - - return delta - } - - Tooltip.prototype.getTitle = function () { - var title - var $e = this.$element - var o = this.options - - title = $e.attr('data-original-title') - || (typeof o.title == 'function' ? o.title.call($e[0]) : o.title) - - return title - } - - Tooltip.prototype.getUID = function (prefix) { - do prefix += ~~(Math.random() * 1000000) - while (document.getElementById(prefix)) - return prefix - } - - Tooltip.prototype.tip = function () { - if (!this.$tip) { - this.$tip = $(this.options.template) - if (this.$tip.length != 1) { - throw new Error(this.type + ' `template` option must consist of exactly 1 top-level element!') - } - } - return this.$tip - } - - Tooltip.prototype.arrow = function () { - return (this.$arrow = this.$arrow || this.tip().find('.tooltip-arrow')) - } - - Tooltip.prototype.enable = function () { - this.enabled = true - } - - Tooltip.prototype.disable = function () { - this.enabled = false - } - - Tooltip.prototype.toggleEnabled = function () { - this.enabled = !this.enabled - } - - Tooltip.prototype.toggle = function (e) { - var self = this - if (e) { - self = $(e.currentTarget).data('bs.' + this.type) - if (!self) { - self = new this.constructor(e.currentTarget, this.getDelegateOptions()) - $(e.currentTarget).data('bs.' + this.type, self) - } - } - - if (e) { - self.inState.click = !self.inState.click - if (self.isInStateTrue()) self.enter(self) - else self.leave(self) - } else { - self.tip().hasClass('in') ? self.leave(self) : self.enter(self) - } - } - - Tooltip.prototype.destroy = function () { - var that = this - clearTimeout(this.timeout) - this.hide(function () { - that.$element.off('.' + that.type).removeData('bs.' + that.type) - if (that.$tip) { - that.$tip.detach() - } - that.$tip = null - that.$arrow = null - that.$viewport = null - }) - } - - - // TOOLTIP PLUGIN DEFINITION - // ========================= - - function Plugin(option) { - return this.each(function () { - var $this = $(this) - var data = $this.data('bs.tooltip') - var options = typeof option == 'object' && option - - if (!data && /destroy|hide/.test(option)) return - if (!data) $this.data('bs.tooltip', (data = new Tooltip(this, options))) - if (typeof option == 'string') data[option]() - }) - } - - var old = $.fn.tooltip - - $.fn.tooltip = Plugin - $.fn.tooltip.Constructor = Tooltip - - - // TOOLTIP NO CONFLICT - // =================== - - $.fn.tooltip.noConflict = function () { - $.fn.tooltip = old - return this - } - -}(jQuery); - -/* ======================================================================== - * Bootstrap: popover.js v3.3.5 - * http://getbootstrap.com/javascript/#popovers - * ======================================================================== - * Copyright 2011-2015 Twitter, Inc. - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) - * ======================================================================== */ - - -+function ($) { - 'use strict'; - - // POPOVER PUBLIC CLASS DEFINITION - // =============================== - - var Popover = function (element, options) { - this.init('popover', element, options) - } - - if (!$.fn.tooltip) throw new Error('Popover requires tooltip.js') - - Popover.VERSION = '3.3.5' - - Popover.DEFAULTS = $.extend({}, $.fn.tooltip.Constructor.DEFAULTS, { - placement: 'right', - trigger: 'click', - content: '', - template: '' - }) - - - // NOTE: POPOVER EXTENDS tooltip.js - // ================================ - - Popover.prototype = $.extend({}, $.fn.tooltip.Constructor.prototype) - - Popover.prototype.constructor = Popover - - Popover.prototype.getDefaults = function () { - return Popover.DEFAULTS - } - - Popover.prototype.setContent = function () { - var $tip = this.tip() - var title = this.getTitle() - var content = this.getContent() - - $tip.find('.popover-title')[this.options.html ? 'html' : 'text'](title) - $tip.find('.popover-content').children().detach().end()[ // we use append for html objects to maintain js events - this.options.html ? (typeof content == 'string' ? 'html' : 'append') : 'text' - ](content) - - $tip.removeClass('fade top bottom left right in') - - // IE8 doesn't accept hiding via the `:empty` pseudo selector, we have to do - // this manually by checking the contents. - if (!$tip.find('.popover-title').html()) $tip.find('.popover-title').hide() - } - - Popover.prototype.hasContent = function () { - return this.getTitle() || this.getContent() - } - - Popover.prototype.getContent = function () { - var $e = this.$element - var o = this.options - - return $e.attr('data-content') - || (typeof o.content == 'function' ? - o.content.call($e[0]) : - o.content) - } - - Popover.prototype.arrow = function () { - return (this.$arrow = this.$arrow || this.tip().find('.arrow')) - } - - - // POPOVER PLUGIN DEFINITION - // ========================= - - function Plugin(option) { - return this.each(function () { - var $this = $(this) - var data = $this.data('bs.popover') - var options = typeof option == 'object' && option - - if (!data && /destroy|hide/.test(option)) return - if (!data) $this.data('bs.popover', (data = new Popover(this, options))) - if (typeof option == 'string') data[option]() - }) - } - - var old = $.fn.popover - - $.fn.popover = Plugin - $.fn.popover.Constructor = Popover - - - // POPOVER NO CONFLICT - // =================== - - $.fn.popover.noConflict = function () { - $.fn.popover = old - return this - } - -}(jQuery); - -/* ======================================================================== - * Bootstrap: scrollspy.js v3.3.5 - * http://getbootstrap.com/javascript/#scrollspy - * ======================================================================== - * Copyright 2011-2015 Twitter, Inc. - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) - * ======================================================================== */ - - -+function ($) { - 'use strict'; - - // SCROLLSPY CLASS DEFINITION - // ========================== - - function ScrollSpy(element, options) { - this.$body = $(document.body) - this.$scrollElement = $(element).is(document.body) ? $(window) : $(element) - this.options = $.extend({}, ScrollSpy.DEFAULTS, options) - this.selector = (this.options.target || '') + ' .nav li > a' - this.offsets = [] - this.targets = [] - this.activeTarget = null - this.scrollHeight = 0 - - this.$scrollElement.on('scroll.bs.scrollspy', $.proxy(this.process, this)) - this.refresh() - this.process() - } - - ScrollSpy.VERSION = '3.3.5' - - ScrollSpy.DEFAULTS = { - offset: 10 - } - - ScrollSpy.prototype.getScrollHeight = function () { - return this.$scrollElement[0].scrollHeight || Math.max(this.$body[0].scrollHeight, document.documentElement.scrollHeight) - } - - ScrollSpy.prototype.refresh = function () { - var that = this - var offsetMethod = 'offset' - var offsetBase = 0 - - this.offsets = [] - this.targets = [] - this.scrollHeight = this.getScrollHeight() - - if (!$.isWindow(this.$scrollElement[0])) { - offsetMethod = 'position' - offsetBase = this.$scrollElement.scrollTop() - } - - this.$body - .find(this.selector) - .map(function () { - var $el = $(this) - var href = $el.data('target') || $el.attr('href') - var $href = /^#./.test(href) && $(href) - - return ($href - && $href.length - && $href.is(':visible') - && [[$href[offsetMethod]().top + offsetBase, href]]) || null - }) - .sort(function (a, b) { return a[0] - b[0] }) - .each(function () { - that.offsets.push(this[0]) - that.targets.push(this[1]) - }) - } - - ScrollSpy.prototype.process = function () { - var scrollTop = this.$scrollElement.scrollTop() + this.options.offset - var scrollHeight = this.getScrollHeight() - var maxScroll = this.options.offset + scrollHeight - this.$scrollElement.height() - var offsets = this.offsets - var targets = this.targets - var activeTarget = this.activeTarget - var i - - if (this.scrollHeight != scrollHeight) { - this.refresh() - } - - if (scrollTop >= maxScroll) { - return activeTarget != (i = targets[targets.length - 1]) && this.activate(i) - } - - if (activeTarget && scrollTop < offsets[0]) { - this.activeTarget = null - return this.clear() - } - - for (i = offsets.length; i--;) { - activeTarget != targets[i] - && scrollTop >= offsets[i] - && (offsets[i + 1] === undefined || scrollTop < offsets[i + 1]) - && this.activate(targets[i]) - } - } - - ScrollSpy.prototype.activate = function (target) { - this.activeTarget = target - - this.clear() - - var selector = this.selector + - '[data-target="' + target + '"],' + - this.selector + '[href="' + target + '"]' - - var active = $(selector) - .parents('li') - .addClass('active') - - if (active.parent('.dropdown-menu').length) { - active = active - .closest('li.dropdown') - .addClass('active') - } - - active.trigger('activate.bs.scrollspy') - } - - ScrollSpy.prototype.clear = function () { - $(this.selector) - .parentsUntil(this.options.target, '.active') - .removeClass('active') - } - - - // SCROLLSPY PLUGIN DEFINITION - // =========================== - - function Plugin(option) { - return this.each(function () { - var $this = $(this) - var data = $this.data('bs.scrollspy') - var options = typeof option == 'object' && option - - if (!data) $this.data('bs.scrollspy', (data = new ScrollSpy(this, options))) - if (typeof option == 'string') data[option]() - }) - } - - var old = $.fn.scrollspy - - $.fn.scrollspy = Plugin - $.fn.scrollspy.Constructor = ScrollSpy - - - // SCROLLSPY NO CONFLICT - // ===================== - - $.fn.scrollspy.noConflict = function () { - $.fn.scrollspy = old - return this - } - - - // SCROLLSPY DATA-API - // ================== - - $(window).on('load.bs.scrollspy.data-api', function () { - $('[data-spy="scroll"]').each(function () { - var $spy = $(this) - Plugin.call($spy, $spy.data()) - }) - }) - -}(jQuery); - -/* ======================================================================== - * Bootstrap: tab.js v3.3.5 - * http://getbootstrap.com/javascript/#tabs - * ======================================================================== - * Copyright 2011-2015 Twitter, Inc. - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) - * ======================================================================== */ - - -+function ($) { - 'use strict'; - - // TAB CLASS DEFINITION - // ==================== - - var Tab = function (element) { - // jscs:disable requireDollarBeforejQueryAssignment - this.element = $(element) - // jscs:enable requireDollarBeforejQueryAssignment - } - - Tab.VERSION = '3.3.5' - - Tab.TRANSITION_DURATION = 150 - - Tab.prototype.show = function () { - var $this = this.element - var $ul = $this.closest('ul:not(.dropdown-menu)') - var selector = $this.data('target') - - if (!selector) { - selector = $this.attr('href') - selector = selector && selector.replace(/.*(?=#[^\s]*$)/, '') // strip for ie7 - } - - if ($this.parent('li').hasClass('active')) return - - var $previous = $ul.find('.active:last a') - var hideEvent = $.Event('hide.bs.tab', { - relatedTarget: $this[0] - }) - var showEvent = $.Event('show.bs.tab', { - relatedTarget: $previous[0] - }) - - $previous.trigger(hideEvent) - $this.trigger(showEvent) - - if (showEvent.isDefaultPrevented() || hideEvent.isDefaultPrevented()) return - - var $target = $(selector) - - this.activate($this.closest('li'), $ul) - this.activate($target, $target.parent(), function () { - $previous.trigger({ - type: 'hidden.bs.tab', - relatedTarget: $this[0] - }) - $this.trigger({ - type: 'shown.bs.tab', - relatedTarget: $previous[0] - }) - }) - } - - Tab.prototype.activate = function (element, container, callback) { - var $active = container.find('> .active') - var transition = callback - && $.support.transition - && ($active.length && $active.hasClass('fade') || !!container.find('> .fade').length) - - function next() { - $active - .removeClass('active') - .find('> .dropdown-menu > .active') - .removeClass('active') - .end() - .find('[data-toggle="tab"]') - .attr('aria-expanded', false) - - element - .addClass('active') - .find('[data-toggle="tab"]') - .attr('aria-expanded', true) - - if (transition) { - element[0].offsetWidth // reflow for transition - element.addClass('in') - } else { - element.removeClass('fade') - } - - if (element.parent('.dropdown-menu').length) { - element - .closest('li.dropdown') - .addClass('active') - .end() - .find('[data-toggle="tab"]') - .attr('aria-expanded', true) - } - - callback && callback() - } - - $active.length && transition ? - $active - .one('bsTransitionEnd', next) - .emulateTransitionEnd(Tab.TRANSITION_DURATION) : - next() - - $active.removeClass('in') - } - - - // TAB PLUGIN DEFINITION - // ===================== - - function Plugin(option) { - return this.each(function () { - var $this = $(this) - var data = $this.data('bs.tab') - - if (!data) $this.data('bs.tab', (data = new Tab(this))) - if (typeof option == 'string') data[option]() - }) - } - - var old = $.fn.tab - - $.fn.tab = Plugin - $.fn.tab.Constructor = Tab - - - // TAB NO CONFLICT - // =============== - - $.fn.tab.noConflict = function () { - $.fn.tab = old - return this - } - - - // TAB DATA-API - // ============ - - var clickHandler = function (e) { - e.preventDefault() - Plugin.call($(this), 'show') - } - - $(document) - .on('click.bs.tab.data-api', '[data-toggle="tab"]', clickHandler) - .on('click.bs.tab.data-api', '[data-toggle="pill"]', clickHandler) - -}(jQuery); - -/* ======================================================================== - * Bootstrap: affix.js v3.3.5 - * http://getbootstrap.com/javascript/#affix - * ======================================================================== - * Copyright 2011-2015 Twitter, Inc. - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) - * ======================================================================== */ - - -+function ($) { - 'use strict'; - - // AFFIX CLASS DEFINITION - // ====================== - - var Affix = function (element, options) { - this.options = $.extend({}, Affix.DEFAULTS, options) - - this.$target = $(this.options.target) - .on('scroll.bs.affix.data-api', $.proxy(this.checkPosition, this)) - .on('click.bs.affix.data-api', $.proxy(this.checkPositionWithEventLoop, this)) - - this.$element = $(element) - this.affixed = null - this.unpin = null - this.pinnedOffset = null - - this.checkPosition() - } - - Affix.VERSION = '3.3.5' - - Affix.RESET = 'affix affix-top affix-bottom' - - Affix.DEFAULTS = { - offset: 0, - target: window - } - - Affix.prototype.getState = function (scrollHeight, height, offsetTop, offsetBottom) { - var scrollTop = this.$target.scrollTop() - var position = this.$element.offset() - var targetHeight = this.$target.height() - - if (offsetTop != null && this.affixed == 'top') return scrollTop < offsetTop ? 'top' : false - - if (this.affixed == 'bottom') { - if (offsetTop != null) return (scrollTop + this.unpin <= position.top) ? false : 'bottom' - return (scrollTop + targetHeight <= scrollHeight - offsetBottom) ? false : 'bottom' - } - - var initializing = this.affixed == null - var colliderTop = initializing ? scrollTop : position.top - var colliderHeight = initializing ? targetHeight : height - - if (offsetTop != null && scrollTop <= offsetTop) return 'top' - if (offsetBottom != null && (colliderTop + colliderHeight >= scrollHeight - offsetBottom)) return 'bottom' - - return false - } - - Affix.prototype.getPinnedOffset = function () { - if (this.pinnedOffset) return this.pinnedOffset - this.$element.removeClass(Affix.RESET).addClass('affix') - var scrollTop = this.$target.scrollTop() - var position = this.$element.offset() - return (this.pinnedOffset = position.top - scrollTop) - } - - Affix.prototype.checkPositionWithEventLoop = function () { - setTimeout($.proxy(this.checkPosition, this), 1) - } - - Affix.prototype.checkPosition = function () { - if (!this.$element.is(':visible')) return - - var height = this.$element.height() - var offset = this.options.offset - var offsetTop = offset.top - var offsetBottom = offset.bottom - var scrollHeight = Math.max($(document).height(), $(document.body).height()) - - if (typeof offset != 'object') offsetBottom = offsetTop = offset - if (typeof offsetTop == 'function') offsetTop = offset.top(this.$element) - if (typeof offsetBottom == 'function') offsetBottom = offset.bottom(this.$element) - - var affix = this.getState(scrollHeight, height, offsetTop, offsetBottom) - - if (this.affixed != affix) { - if (this.unpin != null) this.$element.css('top', '') - - var affixType = 'affix' + (affix ? '-' + affix : '') - var e = $.Event(affixType + '.bs.affix') - - this.$element.trigger(e) - - if (e.isDefaultPrevented()) return - - this.affixed = affix - this.unpin = affix == 'bottom' ? this.getPinnedOffset() : null - - this.$element - .removeClass(Affix.RESET) - .addClass(affixType) - .trigger(affixType.replace('affix', 'affixed') + '.bs.affix') - } - - if (affix == 'bottom') { - this.$element.offset({ - top: scrollHeight - height - offsetBottom - }) - } - } - - - // AFFIX PLUGIN DEFINITION - // ======================= - - function Plugin(option) { - return this.each(function () { - var $this = $(this) - var data = $this.data('bs.affix') - var options = typeof option == 'object' && option - - if (!data) $this.data('bs.affix', (data = new Affix(this, options))) - if (typeof option == 'string') data[option]() - }) - } - - var old = $.fn.affix - - $.fn.affix = Plugin - $.fn.affix.Constructor = Affix - - - // AFFIX NO CONFLICT - // ================= - - $.fn.affix.noConflict = function () { - $.fn.affix = old - return this - } - - - // AFFIX DATA-API - // ============== - - $(window).on('load', function () { - $('[data-spy="affix"]').each(function () { - var $spy = $(this) - var data = $spy.data() - - data.offset = data.offset || {} - - if (data.offsetBottom != null) data.offset.bottom = data.offsetBottom - if (data.offsetTop != null) data.offset.top = data.offsetTop - - Plugin.call($spy, data) - }) - }) - -}(jQuery); +require(["jquery", "modal", "dropdown", "transition"]); \ No newline at end of file diff --git a/static/src/js/lib/bootstrap/dropdown.js b/static/src/js/lib/bootstrap/dropdown.js new file mode 100644 index 0000000..9c732b4 --- /dev/null +++ b/static/src/js/lib/bootstrap/dropdown.js @@ -0,0 +1,168 @@ +define([ 'jquery', 'transition' ], function ( jQuery ) { +/* ======================================================================== + * Bootstrap: dropdown.js v3.3.5 + * http://getbootstrap.com/javascript/#dropdowns + * ======================================================================== + * Copyright 2011-2015 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + * ======================================================================== */ + + ++function ($) { + 'use strict'; + + // DROPDOWN CLASS DEFINITION + // ========================= + + var backdrop = '.dropdown-backdrop' + var toggle = '[data-toggle="dropdown"]' + var Dropdown = function (element) { + $(element).on('click.bs.dropdown', this.toggle) + } + + Dropdown.VERSION = '3.3.5' + + function getParent($this) { + var selector = $this.attr('data-target') + + if (!selector) { + selector = $this.attr('href') + selector = selector && /#[A-Za-z]/.test(selector) && selector.replace(/.*(?=#[^\s]*$)/, '') // strip for ie7 + } + + var $parent = selector && $(selector) + + return $parent && $parent.length ? $parent : $this.parent() + } + + function clearMenus(e) { + if (e && e.which === 3) return + $(backdrop).remove() + $(toggle).each(function () { + var $this = $(this) + var $parent = getParent($this) + var relatedTarget = { relatedTarget: this } + + if (!$parent.hasClass('open')) return + + if (e && e.type == 'click' && /input|textarea/i.test(e.target.tagName) && $.contains($parent[0], e.target)) return + + $parent.trigger(e = $.Event('hide.bs.dropdown', relatedTarget)) + + if (e.isDefaultPrevented()) return + + $this.attr('aria-expanded', 'false') + $parent.removeClass('open').trigger($.Event('hidden.bs.dropdown', relatedTarget)) + }) + } + + Dropdown.prototype.toggle = function (e) { + var $this = $(this) + + if ($this.is('.disabled, :disabled')) return + + var $parent = getParent($this) + var isActive = $parent.hasClass('open') + + clearMenus() + + if (!isActive) { + if ('ontouchstart' in document.documentElement && !$parent.closest('.navbar-nav').length) { + // if mobile we use a backdrop because click events don't delegate + $(document.createElement('div')) + .addClass('dropdown-backdrop') + .insertAfter($(this)) + .on('click', clearMenus) + } + + var relatedTarget = { relatedTarget: this } + $parent.trigger(e = $.Event('show.bs.dropdown', relatedTarget)) + + if (e.isDefaultPrevented()) return + + $this + .trigger('focus') + .attr('aria-expanded', 'true') + + $parent + .toggleClass('open') + .trigger($.Event('shown.bs.dropdown', relatedTarget)) + } + + return false + } + + Dropdown.prototype.keydown = function (e) { + if (!/(38|40|27|32)/.test(e.which) || /input|textarea/i.test(e.target.tagName)) return + + var $this = $(this) + + e.preventDefault() + e.stopPropagation() + + if ($this.is('.disabled, :disabled')) return + + var $parent = getParent($this) + var isActive = $parent.hasClass('open') + + if (!isActive && e.which != 27 || isActive && e.which == 27) { + if (e.which == 27) $parent.find(toggle).trigger('focus') + return $this.trigger('click') + } + + var desc = ' li:not(.disabled):visible a' + var $items = $parent.find('.dropdown-menu' + desc) + + if (!$items.length) return + + var index = $items.index(e.target) + + if (e.which == 38 && index > 0) index-- // up + if (e.which == 40 && index < $items.length - 1) index++ // down + if (!~index) index = 0 + + $items.eq(index).trigger('focus') + } + + + // DROPDOWN PLUGIN DEFINITION + // ========================== + + function Plugin(option) { + return this.each(function () { + var $this = $(this) + var data = $this.data('bs.dropdown') + + if (!data) $this.data('bs.dropdown', (data = new Dropdown(this))) + if (typeof option == 'string') data[option].call($this) + }) + } + + var old = $.fn.dropdown + + $.fn.dropdown = Plugin + $.fn.dropdown.Constructor = Dropdown + + + // DROPDOWN NO CONFLICT + // ==================== + + $.fn.dropdown.noConflict = function () { + $.fn.dropdown = old + return this + } + + + // APPLY TO STANDARD DROPDOWN ELEMENTS + // =================================== + + $(document) + .on('click.bs.dropdown.data-api', clearMenus) + .on('click.bs.dropdown.data-api', '.dropdown form', function (e) { e.stopPropagation() }) + .on('click.bs.dropdown.data-api', toggle, Dropdown.prototype.toggle) + .on('keydown.bs.dropdown.data-api', toggle, Dropdown.prototype.keydown) + .on('keydown.bs.dropdown.data-api', '.dropdown-menu', Dropdown.prototype.keydown) + +}(jQuery); + +}); \ No newline at end of file diff --git a/static/src/js/lib/bootstrap/modal.js b/static/src/js/lib/bootstrap/modal.js new file mode 100644 index 0000000..e5dffc7 --- /dev/null +++ b/static/src/js/lib/bootstrap/modal.js @@ -0,0 +1,340 @@ +define([ 'jquery', 'transition' ], function ( jQuery ) { +/* ======================================================================== + * Bootstrap: modal.js v3.3.5 + * http://getbootstrap.com/javascript/#modals + * ======================================================================== + * Copyright 2011-2015 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + * ======================================================================== */ + + ++function ($) { + 'use strict'; + + // MODAL CLASS DEFINITION + // ====================== + + var Modal = function (element, options) { + this.options = options + this.$body = $(document.body) + this.$element = $(element) + this.$dialog = this.$element.find('.modal-dialog') + this.$backdrop = null + this.isShown = null + this.originalBodyPad = null + this.scrollbarWidth = 0 + this.ignoreBackdropClick = false + + if (this.options.remote) { + this.$element + .find('.modal-content') + .load(this.options.remote, $.proxy(function () { + this.$element.trigger('loaded.bs.modal') + }, this)) + } + } + + Modal.VERSION = '3.3.5' + + Modal.TRANSITION_DURATION = 300 + Modal.BACKDROP_TRANSITION_DURATION = 150 + + Modal.DEFAULTS = { + backdrop: true, + keyboard: true, + show: true + } + + Modal.prototype.toggle = function (_relatedTarget) { + return this.isShown ? this.hide() : this.show(_relatedTarget) + } + + Modal.prototype.show = function (_relatedTarget) { + var that = this + var e = $.Event('show.bs.modal', { relatedTarget: _relatedTarget }) + + this.$element.trigger(e) + + if (this.isShown || e.isDefaultPrevented()) return + + this.isShown = true + + this.checkScrollbar() + this.setScrollbar() + this.$body.addClass('modal-open') + + this.escape() + this.resize() + + this.$element.on('click.dismiss.bs.modal', '[data-dismiss="modal"]', $.proxy(this.hide, this)) + + this.$dialog.on('mousedown.dismiss.bs.modal', function () { + that.$element.one('mouseup.dismiss.bs.modal', function (e) { + if ($(e.target).is(that.$element)) that.ignoreBackdropClick = true + }) + }) + + this.backdrop(function () { + var transition = $.support.transition && that.$element.hasClass('fade') + + if (!that.$element.parent().length) { + that.$element.appendTo(that.$body) // don't move modals dom position + } + + that.$element + .show() + .scrollTop(0) + + that.adjustDialog() + + if (transition) { + that.$element[0].offsetWidth // force reflow + } + + that.$element.addClass('in') + + that.enforceFocus() + + var e = $.Event('shown.bs.modal', { relatedTarget: _relatedTarget }) + + transition ? + that.$dialog // wait for modal to slide in + .one('bsTransitionEnd', function () { + that.$element.trigger('focus').trigger(e) + }) + .emulateTransitionEnd(Modal.TRANSITION_DURATION) : + that.$element.trigger('focus').trigger(e) + }) + } + + Modal.prototype.hide = function (e) { + if (e) e.preventDefault() + + e = $.Event('hide.bs.modal') + + this.$element.trigger(e) + + if (!this.isShown || e.isDefaultPrevented()) return + + this.isShown = false + + this.escape() + this.resize() + + $(document).off('focusin.bs.modal') + + this.$element + .removeClass('in') + .off('click.dismiss.bs.modal') + .off('mouseup.dismiss.bs.modal') + + this.$dialog.off('mousedown.dismiss.bs.modal') + + $.support.transition && this.$element.hasClass('fade') ? + this.$element + .one('bsTransitionEnd', $.proxy(this.hideModal, this)) + .emulateTransitionEnd(Modal.TRANSITION_DURATION) : + this.hideModal() + } + + Modal.prototype.enforceFocus = function () { + $(document) + .off('focusin.bs.modal') // guard against infinite focus loop + .on('focusin.bs.modal', $.proxy(function (e) { + if (this.$element[0] !== e.target && !this.$element.has(e.target).length) { + this.$element.trigger('focus') + } + }, this)) + } + + Modal.prototype.escape = function () { + if (this.isShown && this.options.keyboard) { + this.$element.on('keydown.dismiss.bs.modal', $.proxy(function (e) { + e.which == 27 && this.hide() + }, this)) + } else if (!this.isShown) { + this.$element.off('keydown.dismiss.bs.modal') + } + } + + Modal.prototype.resize = function () { + if (this.isShown) { + $(window).on('resize.bs.modal', $.proxy(this.handleUpdate, this)) + } else { + $(window).off('resize.bs.modal') + } + } + + Modal.prototype.hideModal = function () { + var that = this + this.$element.hide() + this.backdrop(function () { + that.$body.removeClass('modal-open') + that.resetAdjustments() + that.resetScrollbar() + that.$element.trigger('hidden.bs.modal') + }) + } + + Modal.prototype.removeBackdrop = function () { + this.$backdrop && this.$backdrop.remove() + this.$backdrop = null + } + + Modal.prototype.backdrop = function (callback) { + var that = this + var animate = this.$element.hasClass('fade') ? 'fade' : '' + + if (this.isShown && this.options.backdrop) { + var doAnimate = $.support.transition && animate + + this.$backdrop = $(document.createElement('div')) + .addClass('modal-backdrop ' + animate) + .appendTo(this.$body) + + this.$element.on('click.dismiss.bs.modal', $.proxy(function (e) { + if (this.ignoreBackdropClick) { + this.ignoreBackdropClick = false + return + } + if (e.target !== e.currentTarget) return + this.options.backdrop == 'static' + ? this.$element[0].focus() + : this.hide() + }, this)) + + if (doAnimate) this.$backdrop[0].offsetWidth // force reflow + + this.$backdrop.addClass('in') + + if (!callback) return + + doAnimate ? + this.$backdrop + .one('bsTransitionEnd', callback) + .emulateTransitionEnd(Modal.BACKDROP_TRANSITION_DURATION) : + callback() + + } else if (!this.isShown && this.$backdrop) { + this.$backdrop.removeClass('in') + + var callbackRemove = function () { + that.removeBackdrop() + callback && callback() + } + $.support.transition && this.$element.hasClass('fade') ? + this.$backdrop + .one('bsTransitionEnd', callbackRemove) + .emulateTransitionEnd(Modal.BACKDROP_TRANSITION_DURATION) : + callbackRemove() + + } else if (callback) { + callback() + } + } + + // these following methods are used to handle overflowing modals + + Modal.prototype.handleUpdate = function () { + this.adjustDialog() + } + + Modal.prototype.adjustDialog = function () { + var modalIsOverflowing = this.$element[0].scrollHeight > document.documentElement.clientHeight + + this.$element.css({ + paddingLeft: !this.bodyIsOverflowing && modalIsOverflowing ? this.scrollbarWidth : '', + paddingRight: this.bodyIsOverflowing && !modalIsOverflowing ? this.scrollbarWidth : '' + }) + } + + Modal.prototype.resetAdjustments = function () { + this.$element.css({ + paddingLeft: '', + paddingRight: '' + }) + } + + Modal.prototype.checkScrollbar = function () { + var fullWindowWidth = window.innerWidth + if (!fullWindowWidth) { // workaround for missing window.innerWidth in IE8 + var documentElementRect = document.documentElement.getBoundingClientRect() + fullWindowWidth = documentElementRect.right - Math.abs(documentElementRect.left) + } + this.bodyIsOverflowing = document.body.clientWidth < fullWindowWidth + this.scrollbarWidth = this.measureScrollbar() + } + + Modal.prototype.setScrollbar = function () { + var bodyPad = parseInt((this.$body.css('padding-right') || 0), 10) + this.originalBodyPad = document.body.style.paddingRight || '' + if (this.bodyIsOverflowing) this.$body.css('padding-right', bodyPad + this.scrollbarWidth) + } + + Modal.prototype.resetScrollbar = function () { + this.$body.css('padding-right', this.originalBodyPad) + } + + Modal.prototype.measureScrollbar = function () { // thx walsh + var scrollDiv = document.createElement('div') + scrollDiv.className = 'modal-scrollbar-measure' + this.$body.append(scrollDiv) + var scrollbarWidth = scrollDiv.offsetWidth - scrollDiv.clientWidth + this.$body[0].removeChild(scrollDiv) + return scrollbarWidth + } + + + // MODAL PLUGIN DEFINITION + // ======================= + + function Plugin(option, _relatedTarget) { + return this.each(function () { + var $this = $(this) + var data = $this.data('bs.modal') + var options = $.extend({}, Modal.DEFAULTS, $this.data(), typeof option == 'object' && option) + + if (!data) $this.data('bs.modal', (data = new Modal(this, options))) + if (typeof option == 'string') data[option](_relatedTarget) + else if (options.show) data.show(_relatedTarget) + }) + } + + var old = $.fn.modal + + $.fn.modal = Plugin + $.fn.modal.Constructor = Modal + + + // MODAL NO CONFLICT + // ================= + + $.fn.modal.noConflict = function () { + $.fn.modal = old + return this + } + + + // MODAL DATA-API + // ============== + + $(document).on('click.bs.modal.data-api', '[data-toggle="modal"]', function (e) { + var $this = $(this) + var href = $this.attr('href') + var $target = $($this.attr('data-target') || (href && href.replace(/.*(?=#[^\s]+$)/, ''))) // strip for ie7 + var option = $target.data('bs.modal') ? 'toggle' : $.extend({ remote: !/#/.test(href) && href }, $target.data(), $this.data()) + + if ($this.is('a')) e.preventDefault() + + $target.one('show.bs.modal', function (showEvent) { + if (showEvent.isDefaultPrevented()) return // only register focus restorer if modal will actually get shown + $target.one('hidden.bs.modal', function () { + $this.is(':visible') && $this.trigger('focus') + }) + }) + Plugin.call($target, option, this) + }) + +}(jQuery); + +}); \ No newline at end of file diff --git a/static/src/js/lib/bootstrap/transition.js b/static/src/js/lib/bootstrap/transition.js new file mode 100644 index 0000000..77012ec --- /dev/null +++ b/static/src/js/lib/bootstrap/transition.js @@ -0,0 +1,62 @@ +define([ 'jquery', 'transition' ], function ( jQuery ) { +/* ======================================================================== + * Bootstrap: transition.js v3.3.5 + * http://getbootstrap.com/javascript/#transitions + * ======================================================================== + * Copyright 2011-2015 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + * ======================================================================== */ + + ++function ($) { + 'use strict'; + + // CSS TRANSITION SUPPORT (Shoutout: http://www.modernizr.com/) + // ============================================================ + + function transitionEnd() { + var el = document.createElement('bootstrap') + + var transEndEventNames = { + WebkitTransition : 'webkitTransitionEnd', + MozTransition : 'transitionend', + OTransition : 'oTransitionEnd otransitionend', + transition : 'transitionend' + } + + for (var name in transEndEventNames) { + if (el.style[name] !== undefined) { + return { end: transEndEventNames[name] } + } + } + + return false // explicit for ie8 ( ._.) + } + + // http://blog.alexmaccaw.com/css-transitions + $.fn.emulateTransitionEnd = function (duration) { + var called = false + var $el = this + $(this).one('bsTransitionEnd', function () { called = true }) + var callback = function () { if (!called) $($el).trigger($.support.transition.end) } + setTimeout(callback, duration) + return this + } + + $(function () { + $.support.transition = transitionEnd() + + if (!$.support.transition) return + + $.event.special.bsTransitionEnd = { + bindType: $.support.transition.end, + delegateType: $.support.transition.end, + handle: function (e) { + if ($(e.target).is(this)) return e.handleObj.handler.apply(this, arguments) + } + } + }) + +}(jQuery); + +}); \ No newline at end of file diff --git a/static/src/js/lib/datetime_picker/bootstrap-datetimepicker.js b/static/src/js/lib/datetime_picker/bootstrap-datetimepicker.js index b7c76e9..1001fcc 100755 --- a/static/src/js/lib/datetime_picker/bootstrap-datetimepicker.js +++ b/static/src/js/lib/datetime_picker/bootstrap-datetimepicker.js @@ -26,8 +26,15 @@ * * Make it work in bootstrap v3 */ - -!function ($) { +(function(factory){ + if (typeof define === "function" && define.amd) { + define(["jquery"], factory); + } else if (typeof exports === 'object') { + factory(require('jquery')); + } else { + factory(jQuery); + } +}(function ($, undefined) { function UTCDate() { return new Date(Date.UTC.apply(Date, arguments)); @@ -1764,6 +1771,17 @@ ''; $.fn.datetimepicker.DPGlobal = DPGlobal; + $.fn.datetimepicker.dates['zh-CN'] = { + days: ["星期日", "星期一", "星期二", "星期三", "星期四", "星期五", "星期六", "星期日"], + daysShort: ["周日", "周一", "周二", "周三", "周四", "周五", "周六", "周日"], + daysMin: ["日", "一", "二", "三", "四", "五", "六", "日"], + months: ["一月", "二月", "三月", "四月", "五月", "六月", "七月", "八月", "九月", "十月", "十一月", "十二月"], + monthsShort: ["一月", "二月", "三月", "四月", "五月", "六月", "七月", "八月", "九月", "十月", "十一月", "十二月"], + today: "今天", + suffix: [], + meridiem: ["上午", "下午"] + }; + /* DATETIMEPICKER NO CONFLICT * =================== */ @@ -1790,4 +1808,4 @@ $('[data-provide="datetimepicker-inline"]').datetimepicker(); }); -}(window.jQuery); +})); diff --git a/static/src/js/lib/datetime_picker/bootstrap-datetimepicker.zh-CN.js b/static/src/js/lib/datetime_picker/bootstrap-datetimepicker.zh-CN.js index 418fb30..d307644 100755 --- a/static/src/js/lib/datetime_picker/bootstrap-datetimepicker.zh-CN.js +++ b/static/src/js/lib/datetime_picker/bootstrap-datetimepicker.zh-CN.js @@ -2,7 +2,13 @@ * Simplified Chinese translation for bootstrap-datetimepicker * Yuan Cheung */ -;(function($){ +!function(root, factory) { + if (typeof define === 'function' && define.amd) { + define(['jquery', '_datetimePicker'], factory); + } else { + factory(root.jQuery); + } +}(this, function($){ $.fn.datetimepicker.dates['zh-CN'] = { days: ["星期日", "星期一", "星期二", "星期三", "星期四", "星期五", "星期六", "星期日"], daysShort: ["周日", "周一", "周二", "周三", "周四", "周五", "周六", "周日"], @@ -13,4 +19,4 @@ suffix: [], meridiem: ["上午", "下午"] }; -}(jQuery)); +}); diff --git a/static/src/js/lib/simditor/uploader.js b/static/src/js/lib/simditor/uploader.js index d96860e..fc039d7 100644 --- a/static/src/js/lib/simditor/uploader.js +++ b/static/src/js/lib/simditor/uploader.js @@ -143,6 +143,16 @@ Uploader = (function(superClass) { processData: false, contentType: false, type: 'POST', + beforeSend: function(){ + var name = "csrftoken="; + var ca = document.cookie.split(';'); + for (var i = 0; i < ca.length; i++) { + var c = ca[i]; + while (c.charAt(0) == ' ') c = c.substring(1); + if (c.indexOf(name) != -1) name = c.substring(name.length, c.length); + } + arguments[0].setRequestHeader("X-CSRFToken", name); + }, headers: { 'X-File-Name': encodeURIComponent(file.name) }, diff --git a/static/src/js/lib/validator/validator.js b/static/src/js/lib/validator/validator.js index 687d983..f6c523a 100644 --- a/static/src/js/lib/validator/validator.js +++ b/static/src/js/lib/validator/validator.js @@ -25,8 +25,13 @@ * THE SOFTWARE. * ======================================================================== */ - -+function ($) { +!function(root, factory) { + if (typeof define === 'function' && define.amd) { + define(['jquery'], factory); + } else { + factory(root.jQuery); + } +}(this, function ($) { 'use strict'; // VALIDATOR CLASS DEFINITION @@ -322,4 +327,4 @@ }) }) -}(jQuery); +}); diff --git a/static/src/js/utils/editor.js b/static/src/js/utils/editor.js index bb09bb4..53a7d55 100644 --- a/static/src/js/utils/editor.js +++ b/static/src/js/utils/editor.js @@ -8,7 +8,7 @@ define("editor", ["simditor"], function(Simditor){ toolbarFloat: false, defaultImage: null, upload: { - url: "", + url: "/api/admin/upload_image/", params: null, fileKey: "image", connectionCount: 3, diff --git a/submission/migrations/0006_submission_shared.py b/submission/migrations/0006_submission_shared.py new file mode 100644 index 0000000..6d0b4a1 --- /dev/null +++ b/submission/migrations/0006_submission_shared.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('submission', '0005_submission_contest_id'), + ] + + operations = [ + migrations.AddField( + model_name='submission', + name='shared', + field=models.BooleanField(default=False), + ), + ] diff --git a/submission/models.py b/submission/models.py index 6bb5991..b28bd5a 100644 --- a/submission/models.py +++ b/submission/models.py @@ -19,6 +19,8 @@ class Submission(models.Model): accepted_answer_time = models.IntegerField(blank=True, null=True) # 这个字段只有在题目是accepted 的时候才会用到,比赛题目的提交可能还会有得分等信息,存储在这里面 accepted_answer_info = models.TextField(blank=True, null=True) + # 是否可以分享 + shared = models.BooleanField(default=False) class Meta: db_table = "submission" diff --git a/submission/serializers.py b/submission/serializers.py index 8170486..8fd6278 100644 --- a/submission/serializers.py +++ b/submission/serializers.py @@ -22,3 +22,7 @@ class SubmissionSerializer(serializers.ModelSerializer): return User.objects.get(id=obj.user_id).username +class SubmissionhareSerializer(serializers.Serializer): + submission_id = serializers.CharField(max_length=40) + + diff --git a/submission/tests.py b/submission/tests.py index ad3b85a..96d06f0 100644 --- a/submission/tests.py +++ b/submission/tests.py @@ -5,6 +5,7 @@ from django.core.urlresolvers import reverse from account.models import User, REGULAR_USER, ADMIN, SUPER_ADMIN from problem.models import Problem from contest.models import Contest +from contest.models import GROUP_CONTEST, PUBLIC_CONTEST, PASSWORD_PUBLIC_CONTEST from submission.models import Submission from rest_framework.test import APITestCase, APIClient @@ -82,7 +83,8 @@ class SubmissionAPITest(APITestCase): hint="hint1", created_by=User.objects.get(username="test2")) self.global_contest = Contest.objects.create(title="titlex", description="descriptionx", mode=1, - contest_type=2, show_rank=True, show_user_submission=True, + contest_type=PASSWORD_PUBLIC_CONTEST, show_rank=True, + show_user_submission=True, start_time="2015-08-15T10:00:00.000Z", end_time="2015-08-15T12:00:00.000Z", password="aacc", created_by=User.objects.get(username="test2")) @@ -151,7 +153,8 @@ class SubmissionAdminAPITest(APITestCase): hint="hint1", created_by=self.user) self.global_contest = Contest.objects.create(title="titlex", description="descriptionx", mode=1, - contest_type=2, show_rank=True, show_user_submission=True, + contest_type=PASSWORD_PUBLIC_CONTEST, show_rank=True, + show_user_submission=True, start_time="2015-08-15T10:00:00.000Z", end_time="2015-08-15T12:00:00.000Z", password="aacc", created_by=self.user) @@ -190,7 +193,8 @@ class SubmissionPageTest(TestCase): hint="hint1", created_by=User.objects.get(username="test1")) self.global_contest = Contest.objects.create(title="titlex", description="descriptionx", mode=1, - contest_type=2, show_rank=True, show_user_submission=True, + contest_type=PASSWORD_PUBLIC_CONTEST, show_rank=True, + show_user_submission=True, start_time="2015-08-15T10:00:00.000Z", end_time="2015-08-15T12:00:00.000Z", password="aacc", created_by=User.objects.get(username="test1")) diff --git a/submission/views.py b/submission/views.py index 0925e13..6eb69fa 100644 --- a/submission/views.py +++ b/submission/views.py @@ -1,5 +1,6 @@ # coding=utf-8 import json +import logging import redis from django.shortcuts import render @@ -12,14 +13,17 @@ from judge.judger_controller.settings import redis_config from account.decorators import login_required from account.models import SUPER_ADMIN - from problem.models import Problem -from contest.models import ContestProblem - -from utils.shortcuts import serializer_invalid_response, error_response, success_response, error_page, paginate -from .models import Submission -from .serializers import CreateSubmissionSerializer, SubmissionSerializer +from contest.models import ContestProblem, Contest from announcement.models import Announcement +from utils.shortcuts import serializer_invalid_response, error_response, success_response, error_page, paginate + +from .models import Submission +from .serializers import CreateSubmissionSerializer, SubmissionSerializer, SubmissionhareSerializer + + +logger = logging.getLogger("app_info") + class SubmissionAPIView(APIView): @login_required @@ -44,9 +48,14 @@ class SubmissionAPIView(APIView): try: judge.delay(submission.id, problem.time_limit, problem.memory_limit, problem.test_case_id) - except Exception: + except Exception as e: + 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) + request.user.save() # 增加redis 中判题队列长度的计数器 r = redis.Redis(host=redis_config["host"], port=redis_config["port"], db=redis_config["db"]) r.incr("judge_queue_length") @@ -80,24 +89,37 @@ def problem_my_submissions_list_page(request, problem_id): except Problem.DoesNotExist: return error_page(request, u"问题不存在") - submissions = Submission.objects.filter(user_id=request.user.id, problem_id=problem.id, contest_id__isnull=True).order_by("-create_time"). \ + submissions = Submission.objects.filter(user_id=request.user.id, problem_id=problem.id, + contest_id__isnull=True).order_by("-create_time"). \ values("id", "result", "create_time", "accepted_answer_time", "language") return render(request, "oj/problem/my_submissions_list.html", {"submissions": submissions, "problem": problem}) +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 submission.contest_id: + contest = Contest.objects.get(id=submission.contest_id) + # 比赛提交的话,比赛创建者也可见 + if contest.created_by == user: + return submission + raise Submission.DoesNotExist + + @login_required def my_submission(request, submission_id): """ 单个题目的提交详情页 """ try: - # 超级管理员可以查看所有的提交 - if request.user.admin_type != SUPER_ADMIN: - submission = Submission.objects.get(id=submission_id, user_id=request.user.id) - else: - submission = Submission.objects.get(id=submission_id) + submission = _get_submission(submission_id, request.user) except Submission.DoesNotExist: return error_page(request, u"提交不存在") @@ -139,7 +161,7 @@ def my_submission_list_page(request, page=1): 我的所有提交的列表页 """ submissions = Submission.objects.filter(user_id=request.user.id, contest_id__isnull=True). \ - values("id", "result", "create_time", "accepted_answer_time", "language").order_by("-create_time") + values("id", "problem_id", "result", "create_time", "accepted_answer_time", "language").order_by("-create_time") language = request.GET.get("language", None) filter = None if language: @@ -149,6 +171,16 @@ def my_submission_list_page(request, page=1): if result: submissions = submissions.filter(result=int(result)) filter = {"name": "result", "content": result} + + # 为 submission 查询题目 因为提交页面经常会有重复的题目,缓存一下查询结果 + cache_result = {} + for item in submissions: + problem_id = item["problem_id"] + if problem_id not in cache_result: + problem = Problem.objects.get(id=problem_id) + cache_result[problem_id] = problem.title + item["title"] = cache_result[problem_id] + paginator = Paginator(submissions, 20) try: current_page = paginator.page(int(page)) @@ -170,4 +202,20 @@ def my_submission_list_page(request, page=1): return render(request, "oj/submission/my_submissions_list.html", {"submissions": current_page, "page": int(page), "previous_page": previous_page, "next_page": next_page, "start_id": int(page) * 20 - 20, - "announcements": announcements, "filter":filter}) \ No newline at end of file + "announcements": announcements, "filter": filter}) + + +class SubmissionShareAPIView(APIView): + def post(self, request): + serializer = SubmissionhareSerializer(data=request.data) + if serializer.is_valid(): + submission_id = serializer.data["submission_id"] + try: + submission = _get_submission(submission_id, request.user) + except Submission.DoesNotExist: + return error_response(u"提交不存在") + submission.shared = not submission.shared + submission.save() + return success_response(submission.shared) + else: + return serializer_invalid_response(serializer) diff --git a/template/src/admin/admin.html b/template/src/admin/admin.html index 67b9163..0e470c6 100644 --- a/template/src/admin/admin.html +++ b/template/src/admin/admin.html @@ -31,9 +31,9 @@